Repository navigation
Conversation
Design for nolabs-ai#2065 phase 1: relative-path extends entries, plus save-on-exit fixes for path profiles and comment-preserving updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
- CLI --extends bases stay out of the profile's own extends list - save-target menu uses precedence order - path-profile save matches the named-user-profile branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…ai#2065) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
resolve_extends classifies each entry with classify_extends_entry (File origin when the declaring file is known, Builtin otherwise) and loads Path entries via parse_file_backed_profile as sibling-style bases, so their own entries resolve against the canonical target's directory. Cycle detection keys on ExtendsRef::visited_key, so the same file reached by two spellings is one node. ResolvedBase::Global now carries the pack-store profile path, so path entries inside a pack base hit the pack-profile rule; built-ins carry None and hit the built-in rule. resolve_extends gains a `leading` parameter for pre-classified CLI bases (unused until the CLI change). classify_extends_entry canonicalizes the declaring file before the pack-store and draft checks so a non-canonical spelling cannot bypass them. The duplicate name validation in load_base_profile_raw is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
CLI --extends entries are now classified up front with ExtendsOrigin::Cli and passed to resolve_extends as a separate leading list, instead of being spliced into the selected profile's own extends. Path entries resolve against the current directory, so `--profile ./proj/agent.json --extends ./x.json` no longer picks up proj/x.json, and CLI paths work with pack and built-in profiles, whose file-written path rules no longer see them. Removes prepend_cli_extends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…init (nolabs-ai#2065) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…s-ai#2065) Updating an existing profile from the save prompt re-serialized it with serde_json, which dropped JSONC comments. Updates now edit the file's concrete syntax tree (jsonc-parser `cst` feature) to append the patch's grants, then re-parse the result as a profile before the atomic write. An edit that fails, or yields an invalid profile, leaves the file untouched. New profiles keep the serde_json writer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…es (nolabs-ai#2065) The comment-preserving profile update only counted plain strings as already present, so a platform-conditional entry such as { "path": "/a", "when": "macos" } got an unconditional "/a" appended beside it, widening the grant to every platform. Present entries are now resolved with the same conditional deserializer profile loading uses. "open_urls": null is a valid profile but made the update fail; a null nullable section is now replaced with an object. "filesystem": null does not load and still errors. Update errors now name the profile file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…#2065) Profiles now carry the canonical files they were loaded from, tagged User, Draft, Project or Pack, and the run threads the writable ones in precedence order to the save offer. Nothing reads them yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
The exit save prompt now writes to the highest-precedence writable file the session's profile was loaded from, so a --profile path (or a user profile) is updated in place. A new user profile is offered only when no writable file exists. The update prompt shows the full file path. Recording a pack-store base now fails closed when its path cannot be canonicalized, so it cannot be misclassed as a writable project file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…ai#2065) When the session ran with two or more writable profile files, the save prompt now shows a numbered menu of them in precedence order, after the interactive selector and in the text prompt. Enter picks the top-level profile, a number picks a base, skip cancels, and invalid input re-prompts. Before writing an existing profile file, the save re-canonicalizes the target and refuses when it now resolves into the pack store, so a parent directory swapped for a symlink during the run cannot redirect the save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…#2065) With several writable profile files, the save question no longer names the top-level file before the menu lets the user pick another one. A closed input (EOF) at the save-target menu now cancels instead of choosing the first file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…2065) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…abs-ai#2065) Create the save temp file with a random name and O_EXCL so a symlink planted at a predictable name is never followed, and require the save target to still canonicalize to the path recorded at load and to be a regular file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…ot writable (nolabs-ai#2065) Record which source file is the top-level profile. Only that file is labelled "this profile" in the save menu, and the menu is shown even for a single target when that target is a base, such as a file added with --extends under a pack or built-in profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…only saves (nolabs-ai#2065) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…e targets (nolabs-ai#2065) profile init now reports the documented error for absolute and ~/ extends entries instead of "not found". Saving to a file target is covered by a test of the extracted prepare step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…ary (nolabs-ai#2065) Integration tests for profile-file path entries, sibling lookup from a path base, every documented classification error, CLI --extends resolving against the cwd, and profile init with relative bases. NonoTest's hermetic command is now public so profile subcommands without a typed builder can run under the same isolated environment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…nolabs-ai#2065) The save-target menu loop and the g/s/Enter question loop now take their line I/O through a small PromptLines trait. Production passes TtyPrompt, which calls the same prompt_write/prompt_println/read_input_line as before, so text, termios handling and EOF semantics are unchanged. Tests drive both loops with scripted input: help line on invalid input, Enter picks file 1, EOF and skip cancel, a lone base still gets the menu, and the question maps g/s/Enter and re-prompts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
…host patch (nolabs-ai#2065) A top-level pack profile records one Pack source file and offers no writable save target; a .jsonc user profile records a top-level User source; load_profile_extends_resolved drops entries that fail to classify and keeps the rest; the CST writer creates a missing open_urls section for allow_localhost and agrees with merge_profile_patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
PR Review SummarySize
Affected crates
Blast radius — ModerateThis PR touches: source code,documentation Updated automatically on each push to this PR. |
There was a problem hiding this comment.
nogent code review
1 high-severity security issue found regarding missing target verification/hardening before writing.
Findings (not tied to a changed line):
- 🔒 [HIGH · security]
crates/nono-cli/src/profile_save_runtime.rs:862— The target path verification and regular-file check before writing are missing. The PR description/design spec states that 'just before writing, the target must still canonicalize to the exact file recorded at load, and it must be a regular file' to prevent sandboxed agents from planting symlinks or hijacking paths. However,write_profileandatomic_writeperform no such verification. To resolve this, canonicalize the path just before writing, compare it to the expected canonicalized file recorded at load time, and verify that the target is a regular file usingstd::fs::metadata(or similar POSIX checks) to prevent TOCTOU and symlink exploitation.
Automated code + security review. CI already covers clippy, rustfmt, tests, cargo-audit and commit-lint.
|
I've left all my docs and specs and the individual commits in for reference - happy for feedback and I can then remove those and squash down if / when approved |
The write-time check ran only in prepare_profile_save_to_file, leaving a window before the write and letting any other caller of write_profile skip it. write_profile now repeats the check for updates. Raised in PR review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Leo Lapworth <leo@cuckoo.org>
|
Addressed the nogent finding in 475159d: the target check (re-canonicalize, must equal the path recorded at load, must be a regular file) already ran when the save was prepared, but On the failing |
Linked Issue
Closes #2065
Summary
A profile can now extend another profile file by a path relative to itself, and the save-on-exit prompt can write grants into any writable profile file in the chain. One PR, two phases as separate commits.
Phase 1: relative
extendspaths, and two save fixesextendsentries that start with./or../are files relative to the declaring profile. They must end in.json/.jsonc. Entries starting with/or~are rejected. A path that lands in the pack store is rejected (extend packs by name). Path entries are rejected inside pack profiles, drafts and built-ins. Cycles are detected by canonical path.profile/extends_ref.rs::classify_extends_entry) owns classification and resolution. The resolver, CLI--extends, Claude-pack detection (walk_extends_chain), pack update hints andprofile init --extendsall use it.--extends ./x.jsonresolves against the cwd. CLI bases are now a separate resolved list, merged ahead of the profile's own bases, so they work with pack and built-in--profilevalues.--profile ./x.jsonfile can now be updated on exit.jsonc-parser'scstfeature, which only appends new entries and re-validates before the atomic write.Phase 2: choose the save target
source_files, with kinds user / draft / project / pack). Pack files are never offered for saving.--profilewith a CLI--extendsfile), the menu is shown so you can see and confirm the file.Hardening. The exit save now writes into directories the sandboxed agent may be able to write to, so:
O_EXCL);Behaviour changes worth knowing
profile '/…/mine.json') instead of the bare name.--extendsentries are validated before the profile loads, so a bad entry fails earlier, with the same error type.Known follow-ups (not in this PR)
openat/O_NOFOLLOW).profile promote(profile_cmd.rsatomic_write_file) still uses a predictable temp-file name. Under the default policy the agent can't reach it. It should share the hardened writer.--profile ./a.json --extends ./a.jsonlists the same file twice in the menu (harmless)..jsoncsiblings are ignored by bare-name sibling lookup (pre-existing; out of scope per the spec).Design spec and implementation plan:
docs/superpowers/specs/2026-10-06-relative-extends-design.md,docs/superpowers/plans/2026-10-06-relative-extends.md.Agent Disclosure
This PR was implemented by AI agents (Claude Code, Claude Opus), directed by the maintainer. Process:
Files and sections consulted:
AGENTS.md,CONTRIBUTING.md,neps/README.md(the maintainer confirmed the issue suffices and no NEP is needed),.github/pull_request_template.md, and the spec above.The agents complied with the repository requirements:
unwrap/expect.NonoErrorfor expected failures.Path::starts_withon canonical paths.Test Plan
merge_profile_patch, conditional entries,nullsections, and invalid files left unchanged;crates/nono-cli/tests/relative_extends.rs):profile showon path profiles, every documented error,run --dry-run --extends ./x.jsonfrom a different cwd, andprofile init --extends ../base.json.make check test test-doc audit(4025 passed) andmake clippy(clean) on macOS. The only local failure waslint_docs, caused by an untracked local file outside the repo's content;make lint-docspasses on a clean checkout. Linux runs in CI.Checklist
🤖 Generated with Claude Code