|
| 1 | +--- |
| 2 | +date: 2026-06-23 |
| 3 | +topic: namespace-shadowing-policy |
| 4 | +--- |
| 5 | + |
| 6 | +# Namespace Shadowing Policy Requirements |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +Add `shadowing: namespace` as a third Caplet shadowing policy. When two or more participating sources share the same base Caplet ID under this policy, Caplets exposes each side under explicit hash-suffixed, double-underscore qualified IDs and removes the ambiguous bare ID across direct, progressive, Code Mode, and mixed exposure modes. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## Problem Frame |
| 15 | + |
| 16 | +Remote Attach already has a policy boundary for local overlays: a remote Caplet can forbid local shadowing or allow a local Caplet to replace the matching remote one. That binary choice is too coarse for users who intentionally keep matching local and remote Caplets, such as browser or computer-use capabilities that are useful both on the local machine and on a remote host. |
| 17 | + |
| 18 | +The current collision model forces one side to win or one side to disappear. The desired behavior is side-by-side access without making agents guess which runtime a bare Caplet ID means. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Key Decisions |
| 23 | + |
| 24 | +- **Namespace is collision-only.** Non-colliding Caplets keep their existing IDs so normal remote usage does not churn. |
| 25 | +- **No bare ID survives a namespace collision.** Removing the bare ID is the clearest way to avoid a hidden local-vs-remote default. |
| 26 | +- **Qualified IDs use hash-suffixed namespace labels before the double underscore.** Examples such as `remote-a1b2__github` and `local-9f3c__github` avoid dot-target ambiguity, align with existing native tool naming conventions, and reduce generated-ID collision risk. |
| 27 | +- **Default labels come first, with configured aliases in scope.** The first version should work with `remote` and `local` labels by default and allow users to configure namespace labels like `vps` or `mac`; planning decides the config shape. |
| 28 | +- **Hash suffixes must be small and stable.** The suffix must come from durable source identity, must not change across normal restarts or unrelated config edits, and must fail closed with a diagnostic when Caplets cannot determine a durable identity. |
| 29 | +- **Multiple upstreams are supported.** Namespace behavior applies to any same-base-ID collision among participating sources whose relevant upstream policies declare `namespace`, including upstream-vs-upstream collisions with no local Caplet. |
| 30 | +- **The upstream policy remains authoritative.** A local overlay cannot force namespace behavior for an upstream Caplet that still declares `forbid` or `allow` in the first version. |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +## Actors |
| 35 | + |
| 36 | +- A1. **Agent user:** Configures local and remote Caplets and expects predictable access to both runtimes. |
| 37 | +- A2. **Coding agent:** Consumes Caplets through direct tools, progressive operations, Code Mode handles, or mixed exposure modes. |
| 38 | +- A3. **Remote Caplets host:** Publishes upstream Caplets and their shadowing policies through attach and remote-control surfaces. |
| 39 | +- A4. **Local overlay runtime:** Provides local Caplets that may collide with upstream Caplet IDs. |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Requirements |
| 44 | + |
| 45 | +**Policy contract** |
| 46 | + |
| 47 | +- R1. `shadowing: namespace` must be accepted anywhere existing Caplet shadowing policy is accepted. |
| 48 | +- R2. When two or more participating sources share the same base Caplet ID and every affected upstream Caplet declares `shadowing: namespace`, Caplets must expose each source under a qualified ID. |
| 49 | +- R3. Namespace behavior must cover local-vs-upstream and upstream-vs-upstream base-ID collisions. |
| 50 | +- R4. The upstream Caplet's shadowing policy must remain authoritative; local config must not force namespace behavior for upstream Caplets that declare `forbid` or `allow` in the first version. |
| 51 | +- R5. During a namespace collision, Caplets must not expose the unqualified base ID for any colliding source. |
| 52 | +- R6. Caplets that do not collide must keep their normal unqualified IDs. |
| 53 | + |
| 54 | +**Qualified identity** |
| 55 | + |
| 56 | +- R7. Qualified IDs must use a namespace label, a small stable hash suffix, and a double-underscore separator before the base ID, such as `remote-a1b2__github` and `local-9f3c__github`. |
| 57 | +- R8. The default namespace labels must be predictable without additional setup. |
| 58 | +- R9. Users must be able to configure friendly namespace aliases for local and upstream namespaces, such as `vps-a1b2__github` or `mac-9f3c__github`. |
| 59 | +- R10. Configured aliases replace the namespace label for generated qualified IDs rather than creating duplicate handles. |
| 60 | +- R11. Hash suffixes must be derived from durable source identity and must not change across normal restarts or unrelated config edits. |
| 61 | +- R12. Namespace labels and generated qualified IDs must be validated so they do not collide with existing Caplet IDs or each other. |
| 62 | +- R13. If Caplets cannot determine a durable source identity, or if generated qualified IDs still collide after suffixing, namespace exposure must fail closed with a diagnostic rather than falling back to `forbid` behavior. |
| 63 | +- R14. Namespace labels must support multiple upstream sources in the same exposure context. |
| 64 | + |
| 65 | +**Exposure coverage** |
| 66 | + |
| 67 | +- R15. Namespace collision behavior must apply consistently to direct exposure, progressive exposure, Code Mode, and mixed exposure modes. |
| 68 | +- R16. Generated Code Mode handles must use the same resolved qualified Caplet IDs that other surfaces expose. |
| 69 | +- R17. Native direct tool names must remain unambiguous when a qualified Caplet ID is used. |
| 70 | +- R18. CLI inspection, listing, completion, and execution must make the same qualified IDs available that the runtime can execute. |
| 71 | + |
| 72 | +**User feedback and compatibility** |
| 73 | + |
| 74 | +- R19. When a bare ID is removed because of a namespace collision, Caplets must present enough diagnostic information for users to discover the qualified alternatives. |
| 75 | +- R20. Existing `forbid` and `allow` behavior must remain unchanged. |
| 76 | +- R21. Existing non-colliding Caplet IDs must remain stable. |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +## Collision Model |
| 81 | + |
| 82 | +```mermaid |
| 83 | +flowchart TB |
| 84 | + A[Remote Caplet: github] --> C{Any source with same ID?} |
| 85 | + B[Local or another upstream Caplet: github] --> C |
| 86 | + C -->|no| D[Expose github] |
| 87 | + C -->|yes and shadowing: forbid| E[Expose existing forbid behavior] |
| 88 | + C -->|yes and shadowing: allow| F[Expose existing allow behavior] |
| 89 | + C -->|yes and shadowing: namespace| G[Expose remote-a1b2__github and local-9f3c__github] |
| 90 | + G --> H[Do not expose bare github] |
| 91 | +``` |
| 92 | + |
| 93 | +The diagram is conceptual. It describes user-visible identity outcomes, not an implementation sequence. |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## Acceptance Examples |
| 98 | + |
| 99 | +- AE1. **Covers R2, R5, R7, R15.** Given remote `github` has `shadowing: namespace` and local `github` exists, when Caplets exposes capabilities in any supported mode, then `remote-a1b2__github` and `local-9f3c__github` are available and `github` is not. |
| 100 | +- AE2. **Covers R6, R21.** Given remote `linear` has no local or upstream collision, when Caplets lists or exposes capabilities, then `linear` remains the visible ID. |
| 101 | +- AE3. **Covers R18, R19.** Given a user tries to inspect or execute bare `github` during a namespace collision, then Caplets points the user toward the generated qualified IDs rather than silently choosing one. |
| 102 | +- AE4. **Covers R9, R10, R12.** Given the upstream namespace is configured with alias `vps`, when remote `github` collides with local `github`, then the remote qualified ID is `vps-a1b2__github` and no separate `remote-a1b2__github` alias is created for that same Caplet. |
| 103 | +- AE5. **Covers R9, R10, R12.** Given the local namespace is configured with alias `mac`, when remote `github` collides with local `github`, then the local qualified ID is `mac-9f3c__github` and no separate `local-9f3c__github` alias is created for that same Caplet. |
| 104 | +- AE6. **Covers R3, R14.** Given two upstream sources both publish `github`, both affected upstream Caplets declare `shadowing: namespace`, and no local `github` exists, when both upstreams participate in one exposure context, then both upstream Caplets are exposed under distinct hash-suffixed qualified IDs and `github` is not. |
| 105 | +- AE7. **Covers R11, R13.** Given Caplets cannot determine a durable source identity for a colliding namespace source, when namespace exposure is resolved, then Caplets fails closed with a diagnostic rather than exposing a bare ID or falling back to `forbid` behavior. |
| 106 | +- AE8. **Covers R20.** Given remote `github` uses `shadowing: forbid`, when local `github` exists, then existing suppression behavior remains in force. |
| 107 | +- AE9. **Covers R20.** Given remote `github` uses `shadowing: allow`, when local `github` exists, then existing local-wins behavior remains in force. |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +## Scope Boundaries |
| 112 | + |
| 113 | +- Namespacing non-colliding Caplets is out of scope for the first version. |
| 114 | +- Keeping a bare-ID alias during a namespace collision is out of scope. |
| 115 | +- Reframing all remote/local precedence rules beyond the new `namespace` policy is out of scope. |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## Dependencies and Assumptions |
| 120 | + |
| 121 | +- Current policy values are only `forbid` and `allow`; adding `namespace` affects config parsing, runtime config, attach manifests, docs, generated schemas, and tests. |
| 122 | +- Dot-qualified CLI operation targets already use `<caplet>.<operation>`, so dot-based namespace IDs would be ambiguous for single-token CLI targets. |
| 123 | +- Native tool naming already uses double underscores in names such as `caplets__<capletId>`, making double-underscore qualified Caplet IDs a compatible user-facing direction. |
| 124 | +- If the pending remote-list shadowing fix is not merged first, planning should account for CLI list and execution paths that still route by base Caplet ID. |
| 125 | +- Configured namespace aliases are assumed to be a small extension of Caplets config, but planning must still define the exact config shape and validation surface. |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## Outstanding Questions |
| 130 | + |
| 131 | +### Deferred to Planning |
| 132 | + |
| 133 | +- What config shape should declare namespace aliases: source-level aliases, per-Caplet overrides, or both? |
| 134 | +- What exact durable source identity, hash algorithm, and display length should be used for suffixing namespace labels? |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## Sources / Research |
| 139 | + |
| 140 | +- `packages/core/src/config.ts` defines the current `CapletShadowingPolicy` and common shadowing schema. |
| 141 | +- `packages/core/src/config-runtime.ts` mirrors the runtime shadowing policy enum and default. |
| 142 | +- `packages/core/src/attach/api.ts` carries shadowing policy into attach manifest exports. |
| 143 | +- `packages/core/src/native/service.ts` suppresses local Caplets whose remote counterparts do not allow shadowing. |
| 144 | +- `packages/core/src/cli.ts` parses dot-qualified CLI targets and routes remote-mode execution through local overlays by base Caplet ID. |
| 145 | +- `packages/core/src/native/tools.ts` and `packages/core/src/exposure/direct-names.ts` show existing double-underscore native tool naming. |
| 146 | +- `apps/docs/src/content/docs/reference/config.mdx` and `apps/docs/src/content/docs/reference/caplet-files.mdx` expose shadowing policy in generated reference docs. |
0 commit comments