Skip to content

feat: profile inheritance via extends field#203

Open
josephgimenez wants to merge 5 commits intoalways-further:mainfrom
josephgimenez:feature/profile-extends
Open

feat: profile inheritance via extends field#203
josephgimenez wants to merge 5 commits intoalways-further:mainfrom
josephgimenez:feature/profile-extends

Conversation

@josephgimenez
Copy link
Contributor

@josephgimenez josephgimenez commented Mar 2, 2026

Summary

  • Add extends field to user profiles for single-inheritance from built-in or user-defined base profiles
  • Child profiles inherit all settings and only declare additions/overrides, eliminating duplication and preventing drift when base profiles are updated
  • Circular dependency detection, depth limiting (max 10 levels), and missing base errors provide safe failure modes

Closes #164

Usage Examples

Extend a built-in profile with credential injection

Instead of duplicating all of openclaw's paths and settings:

{
  "extends": "openclaw",
  "meta": { "name": "openclaw-creds" },
  "network": {
    "proxy_credentials": ["telegram", "gemini"],
    "custom_credentials": {
      "telegram": {
        "upstream": "https://api.telegram.org",
        "credential_key": "telegram_bot_token",
        "inject_mode": "url_path",
        "path_pattern": "/bot{}/"
      }
    }
  }
}

This inherits openclaw's filesystem paths, workdir config, security groups, and everything else — the child only adds what's new.

Add extra paths to claude-code

{
  "extends": "claude-code",
  "meta": { "name": "claude-code-extended" },
  "filesystem": {
    "allow": ["/opt/my-tools"],
    "read": ["/etc/my-app"]
  }
}

Paths are appended to the base (not replaced), so all of claude-code's original paths remain.

Chain user profiles

~/.config/nono/profiles/team-base.json:

{
  "meta": { "name": "team-base" },
  "filesystem": { "read": ["/shared/configs"] },
  "network": { "block": true }
}

~/.config/nono/profiles/my-dev.json:

{
  "extends": "team-base",
  "meta": { "name": "my-dev" },
  "filesystem": { "allow": ["$HOME/projects"] }
}

my-dev inherits team-base's /shared/configs read access and block: true, then adds its own write path.

Merge strategy

Field Strategy Rationale
meta Child replaces Child defines its own identity
security.groups Append + dedup Accumulate group memberships
security.trust_groups Append + dedup Accumulate exclusions
filesystem.* (6 fields) Append + dedup Accumulate path grants
network.block base || child Restrictive — can't unblock
network.network_profile Child overrides Last writer wins
network.proxy_allow Append + dedup Accumulate allowed hosts
network.proxy_credentials Append + dedup Accumulate services
network.custom_credentials HashMap merge (child wins) Override same key
env_credentials HashMap merge (child wins) Override same key
workdir Child if set, else base None = "not specified" (see note)
hooks HashMap merge (child wins) Override same app
rollback.* Append + dedup Accumulate exclusions
interactive base || child Either activates

Note on WorkdirAccess::None: None currently serves as both "not specified" and "explicitly no access". A child cannot override a base's workdir grant to None. This is a v1 limitation; fixing it requires wrapping in Option<WorkdirAccess> and updating all consumers.

Review remediation

Addressed findings from 20 AI review agents (10 Gemini 3.1, 10 Codex 5.3):

  • dedup_append optimization — store &String references in the HashSet instead of cloning, reducing to one clone per unique item (Gemini inline suggestion)
  • test_extends_user_profile rewrite — original test wrote a base file but the child extended claude-code (built-in), never exercising user-to-user inheritance. Rewritten to parse two temp files and merge them directly, avoiding XDG_CONFIG_HOME races under parallel test execution
  • WorkdirAccess::None limitation — documented the dual-purpose ambiguity in merge_profiles with a code comment
  • Hooks merge test — new test_merge_profiles_merges_hooks covering independent hooks and same-key collision (child wins)
  • Custom credentials collision test — new test_merge_profiles_custom_credentials_child_wins_on_collision verifying child's upstream wins on same-key
  • Trust groups additive test — new test_merge_profiles_trust_groups_additive confirming both base and child trust_groups appear in merged result

Deferred to follow-up

  • WorkdirAccess::None ambiguity (requires public API change to enum + all consumers)
  • load_embedded_policy() caching (OnceLock)
  • Error message improvements (search locations, validation rules)
  • Missing filesystem field test coverage (write, allow_file, write_file)
  • deny_unknown_fields on Profile/ProfileDef structs
  • Post-merge credential validation pass

Test plan

  • 26 new tests covering merge logic, loading pipeline, error cases, and helpers
  • All 309 nono-cli tests pass
  • clippy clean (-D warnings -D clippy::unwrap_used)
  • fmt clean
  • Manual smoke test: extends built-in profile, user-to-user inheritance, three-level chain, circular dependency detection, missing base error

🤖 Generated with Claude Code

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the profile management system by introducing an inheritance mechanism. Users can now define profiles that extend existing ones, inheriting their settings and only specifying additions or overrides. This change aims to streamline profile configuration, minimize redundancy, and prevent configuration drift, all while incorporating robust safety checks to ensure stable and predictable behavior.

Highlights

  • Profile Inheritance: Introduced an extends field to user profiles, allowing single-inheritance from built-in or other user-defined base profiles to reduce duplication and ensure consistency.
  • Robust Error Handling: Implemented mechanisms to detect and prevent circular dependencies in inheritance chains, enforce a maximum inheritance depth (10 levels), and handle cases where a specified base profile is not found.
  • Configurable Merge Strategies: Defined specific merge strategies for various profile fields, including appending and deduplicating for lists (e.g., filesystem paths, security groups), child overriding for metadata, hashmap merging for credentials, and logical OR for boolean flags (e.g., network block, interactive).
  • Comprehensive Testing: Added 23 new tests specifically covering the merge logic, profile loading pipeline, and various error conditions related to inheritance.
Changelog
  • bindings/c/src/lib.rs
    • Mapped the new ProfileInheritance error to NonoErrorCode::ErrProfileParse for C bindings.
  • crates/nono-cli/src/policy.rs
    • Initialized the extends field to None when creating a Profile from a ProfileDef.
  • crates/nono-cli/src/profile/mod.rs
    • Added an extends: Option<String> field to the Profile struct to specify a base profile for inheritance.
    • Refactored profile loading: load_from_file now resolves inheritance, and parse_profile_file was introduced for raw parsing without inheritance resolution.
    • Implemented resolve_extends function to recursively load and merge base profiles, including checks for circular dependencies and maximum inheritance depth.
    • Created load_base_profile_raw to locate and load base profiles from user configurations or embedded policies.
    • Developed merge_profiles function to combine a base and child profile according to defined merge strategies for each field.
    • Added dedup_append helper function for merging string vectors while preserving order and removing duplicates.
    • Included extensive unit tests for profile merging, inheritance resolution, and error scenarios.
  • crates/nono/src/error.rs
    • Added a new ProfileInheritance(String) error variant to handle issues during profile inheritance resolution.
Activity
  • 23 new tests were added to cover the new merge logic, loading pipeline, error cases, and helper functions.
  • All 306 nono-cli tests passed successfully.
  • All 383 nono library tests passed successfully.
  • The codebase is clippy clean with -D warnings -D clippy::unwrap_used.
  • The code formatting is clean.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for profile inheritance using an extends field, which will significantly improve the maintainability and reduce duplication in user profiles. The implementation is robust, including safeguards against circular dependencies and excessive inheritance depth. The merge logic is well-thought-out, and the new functionality is accompanied by a comprehensive set of tests. I have identified a minor performance optimization opportunity and a bug in one of the new tests that I've provided suggestions for. Overall, this is a well-executed feature.

@josephgimenez josephgimenez force-pushed the feature/profile-extends branch from dbaa6da to 8410398 Compare March 2, 2026 02:57
josephgimenez and others added 5 commits March 1, 2026 21:58
Add a dedicated error type for profile inheritance failures (circular
dependencies, depth limits, missing base profiles) to support the
upcoming `extends` field in user profiles.
Signed-off-by: Joseph Gimenez <joseph.gimenez@joingotu.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User profiles can now declare `"extends": "base-profile-name"` to
inherit filesystem paths, security groups, network settings, and all
other fields from a base profile. The child only needs to declare its
additions or overrides, reducing duplication and preventing drift when
base profiles are updated.
Merge strategy:
- Vec fields (filesystem paths, groups): append + dedup
- HashMap fields (credentials, hooks): child wins on key conflict
- Boolean fields (network.block, interactive): OR semantics
- Scalar fields (network_profile, workdir): child overrides
Safety limits: max 10-level inheritance depth, circular dependency
detection via visited-set tracking.
Closes always-further#164
Signed-off-by: Joseph Gimenez <joseph.gimenez@joingotu.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 new tests covering:
- Merge logic: filesystem paths, security groups, deduplication, meta
  replacement, custom credentials, network profile override, network
  block inheritance, workdir inherit/override, env credentials child-wins,
  interactive OR semantics, extends consumed after merge
- Loading pipeline: extends built-in profile, extends user profile,
  three-level chain, missing base error, circular dependency detection,
  self-reference detection, depth limit enforcement, empty child inherits
  all base fields
- Helpers: dedup_append order preservation, empty vec handling
- Deserialization: extends field present and absent
Signed-off-by: Joseph Gimenez <joseph.gimenez@joingotu.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test_extends_user_profile test used `&format!(...)` with no
interpolated variables. Replace with a plain string literal.
Signed-off-by: Joseph Gimenez <joseph.gimenez@joingotu.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Optimize dedup_append to store references in HashSet (one clone per
  item instead of two)
- Rewrite test_extends_user_profile to test actual user-to-user
  inheritance instead of extending a built-in
- Document WorkdirAccess::None dual-purpose limitation in merge logic
- Add tests for hooks merge, custom_credentials collision, and
  trust_groups additive semantics
- Add profile inheritance section to CLI README and CHANGELOG entry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Joseph Gimenez <joseph.gimenez@joingotu.com>
@josephgimenez josephgimenez force-pushed the feature/profile-extends branch from 8410398 to 1227d78 Compare March 2, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Profile Inheritance via extends

1 participant