@deftai/directive-core 0.108.0 → 0.109.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/cache/operations.js +1 -1
  2. package/dist/check/gate-lists.d.ts +14 -0
  3. package/dist/check/gate-lists.js +26 -3
  4. package/dist/check/session-completed-ac.d.ts +1 -1
  5. package/dist/check/session-completed-ac.js +1 -1
  6. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  7. package/dist/consumer-check-contract/evaluate.js +181 -15
  8. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  9. package/dist/delivery-attempt/evaluate.js +69 -0
  10. package/dist/delivery-attempt/index.d.ts +1 -1
  11. package/dist/delivery-attempt/index.js +1 -1
  12. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  13. package/dist/deposit/live-procedure-exclusions.js +110 -0
  14. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  15. package/dist/deposit/live-procedure-targets.js +274 -0
  16. package/dist/deposit/python-free.d.ts +6 -0
  17. package/dist/deposit/python-free.js +15 -0
  18. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  19. package/dist/deposit/rewrite-deposit-links.js +148 -0
  20. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  21. package/dist/deposit/run-stage-content-pack.js +3 -0
  22. package/dist/deposit/stage-content-pack.d.ts +17 -0
  23. package/dist/deposit/stage-content-pack.js +91 -0
  24. package/dist/design-critique/citation-grammar.d.ts +7 -0
  25. package/dist/design-critique/citation-grammar.js +1 -1
  26. package/dist/design-critique/completed-arc-record.d.ts +8 -1
  27. package/dist/design-critique/completed-arc-record.js +98 -14
  28. package/dist/hooks/classify/host-session-identity.d.ts +25 -12
  29. package/dist/hooks/classify/host-session-identity.js +64 -49
  30. package/dist/hooks/classify/index.d.ts +2 -2
  31. package/dist/hooks/classify/index.js +2 -2
  32. package/dist/hooks/classify/paths.d.ts +2 -0
  33. package/dist/hooks/classify/paths.js +8 -4
  34. package/dist/hooks/classify/stdin.js +69 -3
  35. package/dist/hooks/dest-form.d.ts +20 -1
  36. package/dist/hooks/dest-form.js +158 -21
  37. package/dist/hooks/dispatcher.d.ts +22 -2
  38. package/dist/hooks/dispatcher.js +338 -66
  39. package/dist/hooks/fixtures/cases.d.ts +6 -0
  40. package/dist/hooks/fixtures/cases.js +53 -0
  41. package/dist/hooks/git-destructive-log.d.ts +32 -0
  42. package/dist/hooks/git-destructive-log.js +46 -0
  43. package/dist/hooks/index.d.ts +3 -0
  44. package/dist/hooks/index.js +3 -0
  45. package/dist/hooks/owner-liveness.d.ts +92 -0
  46. package/dist/hooks/owner-liveness.js +103 -0
  47. package/dist/hooks/shell-write-targets.d.ts +10 -0
  48. package/dist/hooks/shell-write-targets.js +274 -0
  49. package/dist/hooks/tools.d.ts +49 -2
  50. package/dist/hooks/tools.js +96 -1
  51. package/dist/init-deposit/agent-hooks.d.ts +10 -0
  52. package/dist/init-deposit/agent-hooks.js +39 -0
  53. package/dist/init-deposit/gitignore.d.ts +7 -0
  54. package/dist/init-deposit/gitignore.js +24 -0
  55. package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
  56. package/dist/init-deposit/host-tool-coverage.js +150 -0
  57. package/dist/init-deposit/index.d.ts +1 -0
  58. package/dist/init-deposit/index.js +1 -0
  59. package/dist/init-deposit/init-deposit.js +3 -0
  60. package/dist/init-deposit/refresh.js +7 -0
  61. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  62. package/dist/init-deposit/runtime-writers.js +33 -0
  63. package/dist/intake/issue-ingest.d.ts +6 -1
  64. package/dist/intake/issue-ingest.js +21 -2
  65. package/dist/intake/reconcile-issues.js +13 -13
  66. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  67. package/dist/lifecycle/brief-envelope.js +42 -0
  68. package/dist/lifecycle/index.d.ts +1 -0
  69. package/dist/lifecycle/index.js +1 -0
  70. package/dist/literal-acceptance/evaluate.js +14 -5
  71. package/dist/literal-acceptance/index.d.ts +1 -1
  72. package/dist/literal-acceptance/index.js +1 -1
  73. package/dist/literal-acceptance/run.d.ts +2 -0
  74. package/dist/literal-acceptance/run.js +19 -1
  75. package/dist/orchestration/subagent-monitor.d.ts +6 -0
  76. package/dist/orchestration/subagent-monitor.js +23 -1
  77. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  78. package/dist/orphan-active/candidate-scope.js +157 -0
  79. package/dist/orphan-active/evaluate.d.ts +27 -0
  80. package/dist/orphan-active/evaluate.js +63 -6
  81. package/dist/orphan-active/index.d.ts +1 -0
  82. package/dist/orphan-active/index.js +1 -0
  83. package/dist/policy/merge-approval-head.js +7 -6
  84. package/dist/pr-closing-keywords/gh.js +32 -9
  85. package/dist/pr-closing-keywords/main.d.ts +1 -0
  86. package/dist/pr-closing-keywords/main.js +68 -3
  87. package/dist/pr-closing-keywords/types.d.ts +2 -0
  88. package/dist/pr-merge-readiness/gh.js +32 -9
  89. package/dist/pr-protected-issues/gh.js +36 -9
  90. package/dist/pr-wait-mergeable/wrappers.js +3 -3
  91. package/dist/product-first-done-gate/evaluate.js +6 -2
  92. package/dist/product-first-done-gate/types.js +2 -0
  93. package/dist/release/consumer-hard-stops.d.ts +48 -0
  94. package/dist/release/consumer-hard-stops.js +140 -0
  95. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  96. package/dist/release/consumer-readiness-disclosure.js +51 -0
  97. package/dist/release/index.d.ts +2 -0
  98. package/dist/release/index.js +2 -0
  99. package/dist/release/pipeline.js +55 -0
  100. package/dist/release/run-consumer-readiness.d.ts +15 -0
  101. package/dist/release/run-consumer-readiness.js +24 -0
  102. package/dist/release/types.d.ts +6 -0
  103. package/dist/review-monitor/github-lease.js +4 -3
  104. package/dist/run-summary/types.d.ts +2 -2
  105. package/dist/scm/build-command.d.ts +2 -2
  106. package/dist/scm/build-command.js +2 -2
  107. package/dist/scm/call-shape.d.ts +25 -0
  108. package/dist/scm/call-shape.js +59 -0
  109. package/dist/scm/call.d.ts +7 -4
  110. package/dist/scm/call.js +41 -11
  111. package/dist/scm/gh-rest.d.ts +25 -12
  112. package/dist/scm/gh-rest.js +65 -15
  113. package/dist/scm/index.d.ts +2 -0
  114. package/dist/scm/index.js +2 -0
  115. package/dist/scm/spawn-status.d.ts +33 -0
  116. package/dist/scm/spawn-status.js +53 -0
  117. package/dist/scope/acceptance-evidence.d.ts +1 -1
  118. package/dist/scope/transition.js +4 -14
  119. package/dist/session/ac-pass-banking.d.ts +2 -2
  120. package/dist/session/ac-pass-banking.js +2 -2
  121. package/dist/session/child-occupancy.d.ts +72 -0
  122. package/dist/session/child-occupancy.js +209 -0
  123. package/dist/session/host-session-owner.d.ts +93 -0
  124. package/dist/session/host-session-owner.js +148 -0
  125. package/dist/session/index.d.ts +2 -0
  126. package/dist/session/index.js +2 -0
  127. package/dist/session/occupancy.d.ts +107 -2
  128. package/dist/session/occupancy.js +272 -34
  129. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  130. package/dist/session/verify-ac-session-cache.js +2 -2
  131. package/dist/swarm/complete-cohort.js +2 -0
  132. package/dist/swarm/pre-dispatch.js +2 -0
  133. package/dist/swarm/subagent-status-dir.d.ts +2 -1
  134. package/dist/swarm/subagent-status-dir.js +11 -2
  135. package/dist/swarm/worktrees.js +2 -0
  136. package/dist/triage/evaluate/worktrees.js +153 -6
  137. package/dist/umbrella-current-shape/index.d.ts +51 -3
  138. package/dist/umbrella-current-shape/index.js +106 -18
  139. package/dist/validate-content/deposit-required.d.ts +39 -0
  140. package/dist/validate-content/deposit-required.js +147 -0
  141. package/dist/validate-content/index.d.ts +1 -0
  142. package/dist/validate-content/index.js +1 -0
  143. package/dist/validate-content/validate-links.d.ts +2 -3
  144. package/dist/validate-content/validate-links.js +29 -2
  145. package/dist/vbrief-activate/activate.d.ts +7 -2
  146. package/dist/vbrief-activate/activate.js +26 -13
  147. package/dist/verify-ac/evaluate.d.ts +9 -0
  148. package/dist/verify-ac/evaluate.js +32 -9
  149. package/dist/verify-env/agent-hooks.d.ts +6 -1
  150. package/dist/verify-env/agent-hooks.js +28 -2
  151. package/dist/verify-source/deposit-closure.d.ts +23 -0
  152. package/dist/verify-source/deposit-closure.js +162 -0
  153. package/dist/verify-source/index.d.ts +2 -0
  154. package/dist/verify-source/index.js +2 -0
  155. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  156. package/dist/verify-source/semantic-single-source.js +349 -0
  157. package/package.json +3 -3
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Dispatch-recorded child occupancy leases (#3999).
3
+ *
4
+ * A parent records the child's occupancy owner and the exact worktree root at
5
+ * dispatch in `.deft/child-occupancy/` — lease-gated, not `.deft-scratch/**`.
6
+ * The orchestration terminal transition already carries agent_id / parent_id /
7
+ * phase; this store is the missing occupancy-owner datum. Release reuses
8
+ * `releaseOccupancy` under the occupancy lock and only fires when the recorded
9
+ * child is still the current owner of the recorded tree.
10
+ *
11
+ * Per identity-source kind: `host-env` children are strangers and strand —
12
+ * that is the defect. `payload` parents share one id with their children, so
13
+ * the same transition is a no-op; auto-release would drop a live parent lease
14
+ * mid-flight. Swarm close-out of the launcher's occupancy_session_id is not
15
+ * the precedent and is not copied here.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
18
+ import { join, resolve } from "node:path";
19
+ import { containedRemove, containedWrite } from "../fs/contained-write.js";
20
+ import { hookHostIdentitySource } from "./host-session-owner.js";
21
+ import { stableJson } from "./json.js";
22
+ import { readOccupancy, releaseOccupancy } from "./occupancy.js";
23
+ export const CHILD_OCCUPANCY_SCHEMA_VERSION = 1;
24
+ export const CHILD_OCCUPANCY_DIR = [".deft", "child-occupancy"];
25
+ export const CHILD_OCCUPANCY_IDENTITY_SOURCE_KINDS = ["host-env", "payload"];
26
+ function isIdentitySourceKind(value) {
27
+ return CHILD_OCCUPANCY_IDENTITY_SOURCE_KINDS.includes(value);
28
+ }
29
+ /** Filename-safe agent id; the payload keeps the original. */
30
+ export function childOccupancyFileSegment(agentId) {
31
+ let cleaned = "";
32
+ for (const ch of agentId.trim()) {
33
+ if ((ch >= "A" && ch <= "Z") ||
34
+ (ch >= "a" && ch <= "z") ||
35
+ (ch >= "0" && ch <= "9") ||
36
+ ch === "." ||
37
+ ch === "_" ||
38
+ ch === "-") {
39
+ cleaned += ch;
40
+ }
41
+ else {
42
+ cleaned += "-";
43
+ }
44
+ }
45
+ let start = 0;
46
+ let end = cleaned.length;
47
+ while (start < end && (cleaned[start] === "-" || cleaned[start] === "."))
48
+ start += 1;
49
+ while (end > start && (cleaned[end - 1] === "-" || cleaned[end - 1] === "."))
50
+ end -= 1;
51
+ cleaned = cleaned.slice(start, end);
52
+ return cleaned.length > 0 ? cleaned : "agent";
53
+ }
54
+ export function childOccupancyRelpath(agentId) {
55
+ return [...CHILD_OCCUPANCY_DIR, `${childOccupancyFileSegment(agentId)}.json`];
56
+ }
57
+ export function childOccupancyPath(storeRoot, agentId) {
58
+ return join(resolve(storeRoot), ...childOccupancyRelpath(agentId));
59
+ }
60
+ export function childOccupancyIdentitySourceKind(host) {
61
+ const source = hookHostIdentitySource(host);
62
+ if (source === null)
63
+ return null;
64
+ return source.kind;
65
+ }
66
+ function parseChildOccupancyRecord(payload) {
67
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
68
+ return null;
69
+ const obj = payload;
70
+ const agentId = typeof obj.agent_id === "string" ? obj.agent_id.trim() : "";
71
+ const parentId = typeof obj.parent_id === "string" ? obj.parent_id.trim() : "";
72
+ const occupancyOwner = typeof obj.occupancy_owner === "string" ? obj.occupancy_owner.trim() : "";
73
+ const worktreePath = typeof obj.worktree_path === "string" ? obj.worktree_path.trim() : "";
74
+ const kindRaw = typeof obj.identity_source_kind === "string" ? obj.identity_source_kind.trim() : "";
75
+ if (agentId.length === 0 ||
76
+ parentId.length === 0 ||
77
+ occupancyOwner.length === 0 ||
78
+ worktreePath.length === 0 ||
79
+ !isIdentitySourceKind(kindRaw)) {
80
+ return null;
81
+ }
82
+ return {
83
+ schemaVersion: typeof obj.schemaVersion === "number" ? obj.schemaVersion : CHILD_OCCUPANCY_SCHEMA_VERSION,
84
+ agentId,
85
+ parentId,
86
+ occupancyOwner,
87
+ worktreePath,
88
+ identitySourceKind: kindRaw,
89
+ };
90
+ }
91
+ export function readChildOccupancyLease(storeRoot, agentId) {
92
+ const path = childOccupancyPath(storeRoot, agentId);
93
+ try {
94
+ if (!existsSync(path))
95
+ return null;
96
+ return parseChildOccupancyRecord(JSON.parse(readFileSync(path, { encoding: "utf8" })));
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /**
103
+ * Parent-only write at dispatch. Workers cannot author this store: `.deft/` is
104
+ * not assist-scratch, so a mutation write is occupancy-gated and an assist
105
+ * writer does not get the scratch carve-out.
106
+ */
107
+ export function recordChildOccupancyLease(storeRoot, input) {
108
+ const agentId = input.agentId.trim();
109
+ const parentId = input.parentId.trim();
110
+ const occupancyOwner = input.occupancyOwner.trim();
111
+ const worktreePath = resolve(input.worktreePath.trim());
112
+ if (agentId.length === 0)
113
+ throw new Error("recordChildOccupancyLease needs agentId");
114
+ if (parentId.length === 0)
115
+ throw new Error("recordChildOccupancyLease needs parentId");
116
+ if (occupancyOwner.length === 0) {
117
+ throw new Error("recordChildOccupancyLease needs occupancyOwner");
118
+ }
119
+ if (input.worktreePath.trim().length === 0) {
120
+ throw new Error("recordChildOccupancyLease needs worktreePath");
121
+ }
122
+ const record = {
123
+ schemaVersion: CHILD_OCCUPANCY_SCHEMA_VERSION,
124
+ agentId,
125
+ parentId,
126
+ occupancyOwner,
127
+ worktreePath,
128
+ identitySourceKind: input.identitySourceKind,
129
+ };
130
+ const root = resolve(storeRoot);
131
+ const relpath = childOccupancyRelpath(agentId);
132
+ mkdirSync(join(root, ...CHILD_OCCUPANCY_DIR), { recursive: true });
133
+ containedWrite({
134
+ root,
135
+ target: join(...relpath),
136
+ data: `${stableJson({
137
+ schemaVersion: record.schemaVersion,
138
+ agent_id: record.agentId,
139
+ parent_id: record.parentId,
140
+ occupancy_owner: record.occupancyOwner,
141
+ worktree_path: record.worktreePath,
142
+ identity_source_kind: record.identitySourceKind,
143
+ }, 2)}\n`,
144
+ mode: "replace",
145
+ });
146
+ return record;
147
+ }
148
+ function removeChildOccupancyLease(storeRoot, agentId) {
149
+ const root = resolve(storeRoot);
150
+ const relpath = childOccupancyRelpath(agentId);
151
+ const abs = join(root, ...relpath);
152
+ if (!existsSync(abs))
153
+ return;
154
+ containedRemove({ root, target: join(...relpath) });
155
+ }
156
+ /**
157
+ * Compare-and-release under the occupancy lock. Caller identity is the id the
158
+ * parent recorded at dispatch — not the occupant currently named in the lease
159
+ * file, and not a field on a worker-authored heartbeat.
160
+ */
161
+ export function releaseChildOccupancyOnTerminal(storeRoot, input) {
162
+ const agentId = input.agentId.trim();
163
+ if (agentId.length === 0) {
164
+ return { reason: "missing-record", record: null, occupancy: null };
165
+ }
166
+ const record = readChildOccupancyLease(storeRoot, agentId);
167
+ if (record === null) {
168
+ return { reason: "missing-record", record: null, occupancy: null };
169
+ }
170
+ if (record.identitySourceKind === "payload") {
171
+ return { reason: "payload-skip", record, occupancy: null };
172
+ }
173
+ const tree = resolve(record.worktreePath);
174
+ const now = input.now ?? new Date();
175
+ const live = readOccupancy(tree);
176
+ if (live === null) {
177
+ removeChildOccupancyLease(storeRoot, agentId);
178
+ return { reason: "already-free", record, occupancy: null };
179
+ }
180
+ if (live.sessionId !== record.occupancyOwner) {
181
+ return { reason: "owner-changed", record, occupancy: null };
182
+ }
183
+ const occupancy = releaseOccupancy(tree, {
184
+ sessionId: record.occupancyOwner,
185
+ now,
186
+ env: {},
187
+ lockDeps: input.lockDeps,
188
+ });
189
+ if (occupancy.action === "released") {
190
+ removeChildOccupancyLease(storeRoot, agentId);
191
+ if (resolve(storeRoot) !== tree)
192
+ removeChildOccupancyLease(tree, agentId);
193
+ return { reason: "released", record, occupancy };
194
+ }
195
+ return { reason: "denied", record, occupancy };
196
+ }
197
+ /**
198
+ * Worktree guesses for a heartbeat file. Canonical layout is
199
+ * `<worktree>/.deft-scratch/subagent-status/<agent>.json`; cwd is the fallback
200
+ * when the scratch dir was passed as a custom path.
201
+ */
202
+ export function worktreeCandidatesForHeartbeat(heartbeatPath, cwd) {
203
+ const fromHeartbeat = resolve(heartbeatPath, "..", "..", "..");
204
+ const fromCwd = resolve(cwd);
205
+ if (fromHeartbeat === fromCwd)
206
+ return [fromHeartbeat];
207
+ return [fromHeartbeat, fromCwd];
208
+ }
209
+ //# sourceMappingURL=child-occupancy.js.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Where a host publishes the session id Directive uses as the occupancy owner
3
+ * (#3611 / #3873).
4
+ *
5
+ * This is payload/environment classification, not authentication: cooperative
6
+ * owner ids are locally forgeable by another same-user process. It lives here,
7
+ * outside the hook classifier, because two processes must agree on the answer.
8
+ * The hook resolves it from its own environment; the CLI resolves it from the
9
+ * shell the same host spawned. A claim made under a different id than the write
10
+ * gate later presents is the #3873 defect -- the session denied by its own lease.
11
+ */
12
+ export declare const HOST_IDENTITY_PROVIDERS: readonly ["codex", "claude", "cursor", "grok"];
13
+ export type HookHostIdentityProvider = (typeof HOST_IDENTITY_PROVIDERS)[number];
14
+ export declare const MAX_HOOK_HOST_IDENTITY_UTF8_BYTES = 512;
15
+ /**
16
+ * Where a provider publishes its owner id.
17
+ *
18
+ * `payload` providers carry a verified field on every hook invocation.
19
+ * `host-env` providers publish a stable id in the environment of the processes
20
+ * the host spawns. That environment is host-set: the hook is a sibling of the
21
+ * agent's shell rather than a descendant, so a shell export cannot reach it.
22
+ */
23
+ export type HookHostIdentitySource = {
24
+ readonly kind: "payload";
25
+ readonly field: string;
26
+ } | {
27
+ readonly kind: "host-env";
28
+ readonly variable: string;
29
+ };
30
+ /**
31
+ * Every variable a `host-env` provider publishes.
32
+ *
33
+ * Exported because the ambient step is now read by the CLI occupancy surfaces
34
+ * as well as the hook, so callers that must control the whole ambient identity
35
+ * surface — a hermetic test process, a dispatcher scrubbing a child's
36
+ * environment — need the list rather than the one variable its author knew
37
+ * about (#3954).
38
+ */
39
+ export declare const HOST_ENV_IDENTITY_VARIABLES: readonly string[];
40
+ /** The identity source for a host, or null when the host has no contract. */
41
+ export declare function hookHostIdentitySource(host: string): HookHostIdentitySource | null;
42
+ /** Bound a raw host id by UTF-8 bytes, control characters and surrogate pairing. */
43
+ export declare function isUsableHostSessionId(raw: string): boolean;
44
+ export declare function canonicalHostSessionId(provider: HookHostIdentityProvider, rawSessionId: string): string;
45
+ /**
46
+ * The canonical owner form, derived from the provider list so every surface
47
+ * that checks it moves when a provider is added: the lifecycle-rewrite bridge
48
+ * and grant-time child validation both read this one pattern (#3873 / #3954).
49
+ * Provider ids are lowercase ASCII words, so the alternation needs no escaping.
50
+ */
51
+ export declare const CANONICAL_OWNER_PATTERN: RegExp;
52
+ /**
53
+ * True when a value claims the canonical owner shape, well-formed or not.
54
+ *
55
+ * The `host:` prefix is reserved for host-published identity, so a value under
56
+ * it that is not canonical is a malformed owner rather than an opaque id some
57
+ * session could present (#3954).
58
+ */
59
+ export declare function claimsHostSessionIdShape(value: string): boolean;
60
+ export interface HostSessionIdParts {
61
+ readonly provider: HookHostIdentityProvider;
62
+ readonly rawSessionId: string;
63
+ }
64
+ /**
65
+ * Split a canonical owner back into the provider and the raw id the host
66
+ * published, or null when the value is not canonical.
67
+ *
68
+ * The round-trip check is the point: `host:grok:v1:Z3Jvay1zZXNzaW9uLWF` decodes
69
+ * to the same raw id as `...LWE` does, so without it one session would have two
70
+ * canonical strings and a grant could name the one it never presents (#3954).
71
+ * The raw id is held to the same bound the identity surface applies, so a
72
+ * payload the host could not have published is not accepted here either.
73
+ */
74
+ export declare function parseCanonicalHostSessionId(value: string): HostSessionIdParts | null;
75
+ export type HostEnvIdentityStatus = "ok" | "missing" | "invalid";
76
+ export type HostEnvIdentityResolution = {
77
+ readonly status: "ok";
78
+ readonly rawSessionId: string;
79
+ } | {
80
+ readonly status: "missing" | "invalid";
81
+ readonly rawSessionId: null;
82
+ };
83
+ /** Read one `host-env` provider's variable out of a process environment. */
84
+ export declare function readHostEnvIdentity(environ: NodeJS.ProcessEnv, variable: string): HostEnvIdentityResolution;
85
+ /**
86
+ * The canonical owner the running host published to this process, or null.
87
+ *
88
+ * Used by the CLI claim path so `session:start` binds the lease to the same id
89
+ * the write hook will present. Two live host variables are ambiguous and
90
+ * resolve to null: picking one would bind a claim to a guess.
91
+ */
92
+ export declare function ambientHostSessionOwner(environ?: NodeJS.ProcessEnv): string | null;
93
+ //# sourceMappingURL=host-session-owner.d.ts.map
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Where a host publishes the session id Directive uses as the occupancy owner
3
+ * (#3611 / #3873).
4
+ *
5
+ * This is payload/environment classification, not authentication: cooperative
6
+ * owner ids are locally forgeable by another same-user process. It lives here,
7
+ * outside the hook classifier, because two processes must agree on the answer.
8
+ * The hook resolves it from its own environment; the CLI resolves it from the
9
+ * shell the same host spawned. A claim made under a different id than the write
10
+ * gate later presents is the #3873 defect -- the session denied by its own lease.
11
+ */
12
+ export const HOST_IDENTITY_PROVIDERS = ["codex", "claude", "cursor", "grok"];
13
+ export const MAX_HOOK_HOST_IDENTITY_UTF8_BYTES = 512;
14
+ const HOST_IDENTITY_SOURCES = {
15
+ codex: { kind: "payload", field: "session_id" },
16
+ claude: { kind: "payload", field: "session_id" },
17
+ cursor: { kind: "payload", field: "conversation_id" },
18
+ grok: { kind: "host-env", variable: "GROK_SESSION_ID" },
19
+ };
20
+ /**
21
+ * The `host-env` half of the table above, resolved once.
22
+ *
23
+ * `ambientHostSessionOwner` reads it to build the owner list, and
24
+ * `HOST_ENV_IDENTITY_VARIABLES` projects the variable names out of the same
25
+ * entries, so the production scan and the surface a caller scrubs cannot name
26
+ * different providers (#3954).
27
+ */
28
+ const HOST_ENV_IDENTITY_ENTRIES = HOST_IDENTITY_PROVIDERS.flatMap((provider) => {
29
+ const source = HOST_IDENTITY_SOURCES[provider];
30
+ return source.kind === "host-env" ? [{ provider, variable: source.variable }] : [];
31
+ });
32
+ /**
33
+ * Every variable a `host-env` provider publishes.
34
+ *
35
+ * Exported because the ambient step is now read by the CLI occupancy surfaces
36
+ * as well as the hook, so callers that must control the whole ambient identity
37
+ * surface — a hermetic test process, a dispatcher scrubbing a child's
38
+ * environment — need the list rather than the one variable its author knew
39
+ * about (#3954).
40
+ */
41
+ export const HOST_ENV_IDENTITY_VARIABLES = HOST_ENV_IDENTITY_ENTRIES.map((entry) => entry.variable);
42
+ /** The identity source for a host, or null when the host has no contract. */
43
+ export function hookHostIdentitySource(host) {
44
+ return (HOST_IDENTITY_SOURCES[host] ?? null);
45
+ }
46
+ function hasControlCharacter(value) {
47
+ for (let index = 0; index < value.length; index += 1) {
48
+ const unit = value.charCodeAt(index);
49
+ if (unit <= 0x1f || (unit >= 0x7f && unit <= 0x9f))
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ function hasUnpairedUtf16Surrogate(value) {
55
+ for (let index = 0; index < value.length; index += 1) {
56
+ const unit = value.charCodeAt(index);
57
+ if (unit >= 0xd800 && unit <= 0xdbff) {
58
+ const next = value.charCodeAt(index + 1);
59
+ if (!(next >= 0xdc00 && next <= 0xdfff))
60
+ return true;
61
+ index += 1;
62
+ continue;
63
+ }
64
+ if (unit >= 0xdc00 && unit <= 0xdfff)
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ /** Bound a raw host id by UTF-8 bytes, control characters and surrogate pairing. */
70
+ export function isUsableHostSessionId(raw) {
71
+ return (raw.length > 0 &&
72
+ raw === raw.trim() &&
73
+ !hasControlCharacter(raw) &&
74
+ !hasUnpairedUtf16Surrogate(raw) &&
75
+ Buffer.byteLength(raw, "utf8") <= MAX_HOOK_HOST_IDENTITY_UTF8_BYTES);
76
+ }
77
+ export function canonicalHostSessionId(provider, rawSessionId) {
78
+ const encoded = Buffer.from(rawSessionId, "utf8").toString("base64url");
79
+ return `host:${provider}:v1:${encoded}`;
80
+ }
81
+ /**
82
+ * The canonical owner form, derived from the provider list so every surface
83
+ * that checks it moves when a provider is added: the lifecycle-rewrite bridge
84
+ * and grant-time child validation both read this one pattern (#3873 / #3954).
85
+ * Provider ids are lowercase ASCII words, so the alternation needs no escaping.
86
+ */
87
+ export const CANONICAL_OWNER_PATTERN = new RegExp(`^host:(?:${HOST_IDENTITY_PROVIDERS.join("|")}):v1:[A-Za-z0-9_-]+$`);
88
+ /**
89
+ * True when a value claims the canonical owner shape, well-formed or not.
90
+ *
91
+ * The `host:` prefix is reserved for host-published identity, so a value under
92
+ * it that is not canonical is a malformed owner rather than an opaque id some
93
+ * session could present (#3954).
94
+ */
95
+ export function claimsHostSessionIdShape(value) {
96
+ return value.startsWith("host:");
97
+ }
98
+ /**
99
+ * Split a canonical owner back into the provider and the raw id the host
100
+ * published, or null when the value is not canonical.
101
+ *
102
+ * The round-trip check is the point: `host:grok:v1:Z3Jvay1zZXNzaW9uLWF` decodes
103
+ * to the same raw id as `...LWE` does, so without it one session would have two
104
+ * canonical strings and a grant could name the one it never presents (#3954).
105
+ * The raw id is held to the same bound the identity surface applies, so a
106
+ * payload the host could not have published is not accepted here either.
107
+ */
108
+ export function parseCanonicalHostSessionId(value) {
109
+ if (!CANONICAL_OWNER_PATTERN.test(value))
110
+ return null;
111
+ const segments = value.split(":");
112
+ const provider = segments[1];
113
+ const encoded = segments[3] ?? "";
114
+ const rawSessionId = Buffer.from(encoded, "base64url").toString("utf8");
115
+ if (!isUsableHostSessionId(rawSessionId))
116
+ return null;
117
+ if (canonicalHostSessionId(provider, rawSessionId) !== value)
118
+ return null;
119
+ return { provider, rawSessionId };
120
+ }
121
+ /** Read one `host-env` provider's variable out of a process environment. */
122
+ export function readHostEnvIdentity(environ, variable) {
123
+ const raw = environ[variable];
124
+ // An unset variable and an empty one are one state to a host: `set X=` on
125
+ // Windows deletes the entry, so absence must not read as a malformed value.
126
+ if (raw === undefined || raw.length === 0)
127
+ return { status: "missing", rawSessionId: null };
128
+ return isUsableHostSessionId(raw)
129
+ ? { status: "ok", rawSessionId: raw }
130
+ : { status: "invalid", rawSessionId: null };
131
+ }
132
+ /**
133
+ * The canonical owner the running host published to this process, or null.
134
+ *
135
+ * Used by the CLI claim path so `session:start` binds the lease to the same id
136
+ * the write hook will present. Two live host variables are ambiguous and
137
+ * resolve to null: picking one would bind a claim to a guess.
138
+ */
139
+ export function ambientHostSessionOwner(environ = process.env) {
140
+ const resolved = [];
141
+ for (const { provider, variable } of HOST_ENV_IDENTITY_ENTRIES) {
142
+ const value = readHostEnvIdentity(environ, variable);
143
+ if (value.status === "ok")
144
+ resolved.push(canonicalHostSessionId(provider, value.rawSessionId));
145
+ }
146
+ return resolved.length === 1 ? resolved[0] : null;
147
+ }
148
+ //# sourceMappingURL=host-session-owner.js.map
@@ -2,10 +2,12 @@ export * from "./ac-pass-banking.js";
2
2
  export * from "./ac-pass-reuse.js";
3
3
  export * from "./active-cli.js";
4
4
  export * from "./ceremony-dial-evidence.js";
5
+ export * from "./child-occupancy.js";
5
6
  export * from "./compact-ritual.js";
6
7
  export * from "./deposit-sha.js";
7
8
  export * from "./effort-budget.js";
8
9
  export * from "./git.js";
10
+ export * from "./host-session-owner.js";
9
11
  export * from "./json.js";
10
12
  export * from "./occupancy.js";
11
13
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -2,10 +2,12 @@ export * from "./ac-pass-banking.js";
2
2
  export * from "./ac-pass-reuse.js";
3
3
  export * from "./active-cli.js";
4
4
  export * from "./ceremony-dial-evidence.js";
5
+ export * from "./child-occupancy.js";
5
6
  export * from "./compact-ritual.js";
6
7
  export * from "./deposit-sha.js";
7
8
  export * from "./effort-budget.js";
8
9
  export * from "./git.js";
10
+ export * from "./host-session-owner.js";
9
11
  export * from "./json.js";
10
12
  export * from "./occupancy.js";
11
13
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -19,6 +19,25 @@
19
19
  * composite hook write gate measures the tree's verified ritual owner against
20
20
  * the occupant that issued the grant, not against the writer.
21
21
  *
22
+ * Parent and child, answered per identity-source kind (#3954, and it does not
23
+ * have one answer). On a `host-env` host the parent and its dispatched children
24
+ * are different actors, because the host publishes a different id into each
25
+ * agent session. The answer there is identity, not automatic membership: each
26
+ * side resolves its own owner through the shared lookup chain below and claims
27
+ * its own worktree, which is where the dispatch envelope already puts it.
28
+ * Membership stays explicit and owner-issued for the deliberate same-tree case,
29
+ * and it stays affordable only that way -- 32 grants at a four-hour TTL against
30
+ * a twenty-minute lease means granting on every dispatch exhausts a busy
31
+ * parent's lease inside a day. The revocation trigger is therefore the owner's
32
+ * own `occupancy:grant --revoke`, or expiry; releasing a child's lease on its
33
+ * terminal event is dispatcher lifecycle in `child-occupancy.ts` (#3999).
34
+ * On a `payload` host parent and subagents share one id, so there is no foreign
35
+ * child lease to admit and nothing to grant -- and the live consequence is the
36
+ * inverse one: `owns` is true for both, so a parent's `occupancy:release`
37
+ * removes a working child's lease mid-flight with no denial. That is a property
38
+ * of shared host identity, not of this module; a bearer boundary cannot
39
+ * distinguish two processes presenting one string.
40
+ *
22
41
  * Concurrency model:
23
42
  * - Assumptions: local filesystem; cooperating processes on one machine.
24
43
  * - Guarantees: mutual exclusion under crash-free operation; detect-and-abort
@@ -218,7 +237,18 @@ export declare function formatOccupancyStaleWarning(record: OccupancyRecord, now
218
237
  * lease is gone rather than merely quiet, so the answer is to re-claim.
219
238
  */
220
239
  export declare function formatOccupancyAgeCapRemediation(record: OccupancyRecord, now?: Date, maxLeaseMs?: number): string;
221
- export declare function formatOccupancyRemediation(record: OccupancyRecord, now?: Date): string;
240
+ /**
241
+ * Tell a refused caller who holds the lease and what it can actually run.
242
+ *
243
+ * `presented` is the id the refused caller offered (#3873). Without it the
244
+ * message can only print `<your-session-id>` placeholders, which is fine for a
245
+ * CLI caller that passed its own `--session-id` and useless to a hook process,
246
+ * which does not know what identity it presented. Passing it also keeps the
247
+ * message honest when there is none: a grant cannot name an empty child --
248
+ * `occupancy:grant --child-session-id=` is refused at parse and at membership --
249
+ * so that remediation is not printed to a caller who could never run it.
250
+ */
251
+ export declare function formatOccupancyRemediation(record: OccupancyRecord, now?: Date, presented?: string): string;
222
252
  /**
223
253
  * Refuse an administrative verb to a granted child (#3755). Named apart from
224
254
  * the stranger refusal because the answer differs: this caller is admitted, and
@@ -226,11 +256,78 @@ export declare function formatOccupancyRemediation(record: OccupancyRecord, now?
226
256
  * derives from. A grant admits writes; the lease has one owner.
227
257
  */
228
258
  export declare function formatOccupancyMemberAdministrationRefusal(record: OccupancyRecord, grant: OccupancyGrant, verb: string): string;
259
+ /** Which step of the shared lookup chain produced the actor (#3954). */
260
+ export type PresentedIdentitySource = "explicit" | "environment" | "host" | "none";
261
+ export interface PresentedIdentity {
262
+ /** The id this surface acts under; empty when nothing was presented. */
263
+ readonly sessionId: string;
264
+ readonly source: PresentedIdentitySource;
265
+ /**
266
+ * The owner the running host published, when it names a different session
267
+ * than `sessionId` does; otherwise null. This is the claimer-versus-presenter
268
+ * split itself: the id a session claims under and the id its hook process
269
+ * presents are two different sessions (#3954).
270
+ */
271
+ readonly disagreeingHostOwner: string | null;
272
+ }
273
+ /**
274
+ * The one lookup order every occupancy surface shares (#3954): an explicit
275
+ * `--session-id`, then `DEFT_SESSION_ID`, then the owner the running host
276
+ * published.
277
+ *
278
+ * The terminal is the caller's, not this function's. Claim mints, because
279
+ * claiming establishes an identity where none exists. Release, heartbeat and
280
+ * grant/revoke are proving one, so they take the empty string and keep the
281
+ * diagnosis written for it -- a shared mint would replace "you presented
282
+ * nothing" with a plausible id no later hook will ever present, on every host
283
+ * that publishes no owner of its own.
284
+ *
285
+ * Disagreement is reported, not resolved. The order stands, so an explicit id
286
+ * beats the environment and the environment beats the host; what changes is
287
+ * that a refused caller is told the host names someone else, which is the state
288
+ * a stale inherited `DEFT_SESSION_ID` produces and the one an operator cannot
289
+ * otherwise see.
290
+ */
291
+ export declare function resolvePresentedIdentity(input?: {
292
+ readonly sessionId?: string;
293
+ readonly env?: NodeJS.ProcessEnv;
294
+ }): PresentedIdentity;
295
+ /**
296
+ * Name a claimer-versus-presenter split on a refusal, or return "" (#3954).
297
+ *
298
+ * Appended only to denials: while the caller is admitted the split costs it
299
+ * nothing, and on a refusal it is the one fact that explains why an id the
300
+ * operator believes is theirs is being treated as a stranger's.
301
+ */
302
+ export declare function formatPresentedIdentityDisagreement(identity: PresentedIdentity): string;
303
+ /**
304
+ * The owner a claim is made under: the shared lookup chain, then a mint.
305
+ *
306
+ * The host step is what makes an identified host's claim reachable (#3873).
307
+ * Minting instead binds the lease to an id no later hook process can present,
308
+ * so the session that claimed the worktree is refused by its own lease. The
309
+ * mint stays as the last resort for hosts that publish nothing, and it is the
310
+ * one terminal the prove-surfaces deliberately do not share (#3954).
311
+ */
229
312
  export declare function resolveOccupancySessionId(input?: ApplyOccupancyInput): string;
230
313
  export declare function readOccupancy(projectRoot: string): OccupancyRecord | null;
231
314
  export declare function liveOccupant(projectRoot: string, now?: Date, ttlMs?: number, maxLeaseMs?: number): OccupancyRecord | null;
232
315
  export declare function applyWorktreeOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
233
316
  export declare function stealOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
317
+ /**
318
+ * Release the caller's own lease.
319
+ *
320
+ * Owner-only, deliberately (#3954 item 4, answering the open question the
321
+ * design-critique arc left for the builder). Letting an unidentified caller
322
+ * release the occupant the lease file itself records would make possession of
323
+ * that file path into authority to delete a live lease, which is exactly what
324
+ * the `!expired && !owns` refusal exists to prevent -- and the cooperative
325
+ * bearer model (#3755) has no second check behind it. The unreachable printed
326
+ * recovery is fixed by the shared lookup chain instead: on a host that
327
+ * publishes an owner, the occupant now resolves itself and a bare
328
+ * `occupancy:release` is the occupant, so the message the deny prints is one
329
+ * the party it addresses can actually run.
330
+ */
234
331
  export declare function releaseOccupancy(projectRoot: string, input?: {
235
332
  readonly sessionId?: string;
236
333
  readonly env?: NodeJS.ProcessEnv;
@@ -313,7 +410,15 @@ export declare function evaluateOccupancyWriteGate(projectRoot: string, input?:
313
410
  * Never claims and never mints an owner — an unheld or foreign lease is denied.
314
411
  */
315
412
  export declare function heartbeatOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
316
- /** Close-out identity comes from the launch manifest or DEFT_SESSION_ID — never occupancy.json. */
413
+ /**
414
+ * Close-out identity comes from the launch manifest, `DEFT_SESSION_ID`, or the
415
+ * owner the running host published — never occupancy.json (#3954).
416
+ *
417
+ * Reading the lease for identity would be the anonymous recorded-occupant
418
+ * release refused in `releaseOccupancy`; the host step is the same shared
419
+ * lookup chain every other occupancy surface uses, so a cohort launched on a
420
+ * host that publishes an owner can close out without an explicit id.
421
+ */
317
422
  export declare function releaseSwarmOccupancy(projectRoot: string, input?: {
318
423
  readonly sessionId?: string;
319
424
  readonly env?: NodeJS.ProcessEnv;