Skip to content

Commit 907dbd5

Browse files
authored
feat(core): add namespace shadowing policy (#147)
* fix(cli): respect remote shadowing policy in list * feat(core): add namespace shadowing policy * fix(cli): honor shared remote shadowing fallback * revert(cli): remove shared remote shadowing fallback * fix(core): address namespace shadowing review * fix(core): harden namespace direct diagnostics
1 parent e71eff0 commit 907dbd5

33 files changed

Lines changed: 2044 additions & 103 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@caplets/core": patch
3+
"caplets": patch
4+
---
5+
6+
Respect remote Caplet shadowing policy when merging local overlays into remote CLI list output.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@caplets/core": minor
3+
---
4+
5+
Add namespace shadowing policy support with source-level aliases and native remote/local qualified IDs.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ benchmark-results/
3838

3939
# worktrees
4040
.worktrees/
41+
42+
# brag
43+
brag-output/

CONCEPTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Shared domain vocabulary for this project -- entities, named processes, and stat
88

99
A configured capability surface that exposes a backend to agents through a stable handle, progressive wrapper tools, or direct tool operations.
1010

11+
### Namespace Shadowing Policy
12+
13+
A Caplet shadowing policy where a local/upstream ID collision exposes both Caplets under qualified namespace IDs and removes the ambiguous bare ID.
14+
15+
Namespace Shadowing is collision-only: non-colliding Caplets keep their normal IDs, while colliding Caplets use explicit, hash-suffixed runtime labels such as `remote-a1b2` and `local-9f3c` before the double-underscore Caplet ID separator. Runtime labels may have configured aliases, but aliases replace the namespace label instead of creating duplicate handles. Hash suffixes must be small, stable, and derived from durable source identity; unresolved generated-ID collisions fail closed with diagnostics rather than falling back to forbidden shadowing behavior.
16+
1117
### Caplets Daemon
1218

1319
A per-user native service managed by `caplets daemon` that runs local HTTP `caplets serve` through the operating system service manager.

apps/docs/src/content/docs/reference/caplet-files.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Use this Caplet when an agent needs the current repository's local test signal.
6565
| `description` | Required | string | Compact capability description shown before the full Caplet card is disclosed. |
6666
| `tags` | Optional | array | Optional tags for grouping or searching Caplets. |
6767
| `exposure` | Optional | "direct" \| "progressive" \| "code_mode" \| "direct_and_code_mode" \| "progressive_and_code_mode" | How this Caplet is exposed to agents. |
68-
| `shadowing` | Optional | "forbid" \| "allow" | Whether attached local Caplets may shadow this remote Caplet ID. |
68+
| `shadowing` | Optional | "forbid" \| "allow" \| "namespace" | Whether attached local Caplets may shadow this remote Caplet ID. |
6969
| `useWhen` | Optional | string | When agents should prefer this Caplet or configured action. |
7070
| `avoidWhen` | Optional | string | When agents should avoid this Caplet or configured action. |
7171
| `setup` | Optional | object | Optional explicit setup and verification metadata for this Caplet. |

apps/docs/src/content/docs/reference/config.mdx

Lines changed: 23 additions & 22 deletions
Large diffs are not rendered by default.

apps/landing/public/caplet.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
},
4242
"shadowing": {
4343
"type": "string",
44-
"enum": ["forbid", "allow"],
44+
"enum": ["forbid", "allow", "namespace"],
4545
"description": "Whether attached local Caplets may shadow this remote Caplet ID."
4646
},
4747
"useWhen": {

apps/landing/public/config.schema.json

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,35 @@
102102
},
103103
"additionalProperties": false
104104
},
105+
"namespaceAliases": {
106+
"default": {
107+
"upstreams": {}
108+
},
109+
"description": "Source-level namespace aliases for hash-qualified Caplet IDs.",
110+
"type": "object",
111+
"properties": {
112+
"local": {
113+
"type": "string",
114+
"pattern": "^[a-z](?:[a-z0-9-]{0,30}[a-z0-9])?$",
115+
"description": "Namespace label used when qualifying colliding Caplet IDs."
116+
},
117+
"upstreams": {
118+
"default": {},
119+
"description": "Namespace aliases keyed by durable upstream source identity.",
120+
"type": "object",
121+
"propertyNames": {
122+
"type": "string",
123+
"minLength": 1
124+
},
125+
"additionalProperties": {
126+
"type": "string",
127+
"pattern": "^[a-z](?:[a-z0-9-]{0,30}[a-z0-9])?$",
128+
"description": "Namespace label used when qualifying colliding Caplet IDs."
129+
}
130+
}
131+
},
132+
"additionalProperties": false
133+
},
105134
"mcpServers": {
106135
"default": {},
107136
"description": "Downstream MCP servers keyed by stable server ID.",
@@ -351,7 +380,7 @@
351380
"default": "forbid",
352381
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
353382
"type": "string",
354-
"enum": ["forbid", "allow"]
383+
"enum": ["forbid", "allow", "namespace"]
355384
},
356385
"useWhen": {
357386
"description": "When agents should prefer this Caplet or configured action.",
@@ -773,7 +802,7 @@
773802
"default": "forbid",
774803
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
775804
"type": "string",
776-
"enum": ["forbid", "allow"]
805+
"enum": ["forbid", "allow", "namespace"]
777806
},
778807
"useWhen": {
779808
"description": "When agents should prefer this Caplet or configured action.",
@@ -1204,7 +1233,7 @@
12041233
"default": "forbid",
12051234
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
12061235
"type": "string",
1207-
"enum": ["forbid", "allow"]
1236+
"enum": ["forbid", "allow", "namespace"]
12081237
},
12091238
"useWhen": {
12101239
"description": "When agents should prefer this Caplet or configured action.",
@@ -1670,7 +1699,7 @@
16701699
"default": "forbid",
16711700
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
16721701
"type": "string",
1673-
"enum": ["forbid", "allow"]
1702+
"enum": ["forbid", "allow", "namespace"]
16741703
},
16751704
"useWhen": {
16761705
"description": "When agents should prefer this Caplet or configured action.",
@@ -2186,7 +2215,7 @@
21862215
"default": "forbid",
21872216
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
21882217
"type": "string",
2189-
"enum": ["forbid", "allow"]
2218+
"enum": ["forbid", "allow", "namespace"]
21902219
},
21912220
"useWhen": {
21922221
"description": "When agents should prefer this Caplet or configured action.",
@@ -2551,7 +2580,7 @@
25512580
"default": "forbid",
25522581
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
25532582
"type": "string",
2554-
"enum": ["forbid", "allow"]
2583+
"enum": ["forbid", "allow", "namespace"]
25552584
},
25562585
"useWhen": {
25572586
"description": "When agents should prefer this Caplet or configured action.",
@@ -2814,7 +2843,7 @@
28142843
"default": "forbid",
28152844
"description": "Whether attached local Caplets may shadow this remote Caplet ID.",
28162845
"type": "string",
2817-
"enum": ["forbid", "allow"]
2846+
"enum": ["forbid", "allow", "namespace"]
28182847
},
28192848
"useWhen": {
28202849
"description": "When agents should prefer this Caplet or configured action.",
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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

Comments
 (0)