@deftai/directive-core 0.109.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.
- package/dist/design-critique/completed-arc-record.d.ts +8 -1
- package/dist/design-critique/completed-arc-record.js +43 -4
- package/dist/hooks/classify/host-session-identity.d.ts +1 -1
- package/dist/hooks/classify/host-session-identity.js +23 -5
- package/dist/hooks/dispatcher.d.ts +17 -1
- package/dist/hooks/dispatcher.js +83 -7
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/owner-liveness.d.ts +92 -0
- package/dist/hooks/owner-liveness.js +103 -0
- package/dist/hooks/tools.d.ts +47 -18
- package/dist/hooks/tools.js +82 -16
- package/dist/init-deposit/agent-hooks.d.ts +10 -0
- package/dist/init-deposit/agent-hooks.js +39 -0
- package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
- package/dist/init-deposit/host-tool-coverage.js +150 -0
- package/dist/init-deposit/index.d.ts +1 -0
- package/dist/init-deposit/index.js +1 -0
- package/dist/orchestration/subagent-monitor.d.ts +6 -0
- package/dist/orchestration/subagent-monitor.js +23 -1
- package/dist/session/child-occupancy.d.ts +72 -0
- package/dist/session/child-occupancy.js +209 -0
- package/dist/session/host-session-owner.d.ts +40 -0
- package/dist/session/host-session-owner.js +64 -5
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +89 -4
- package/dist/session/occupancy.js +211 -31
- package/dist/swarm/complete-cohort.js +2 -0
- package/dist/swarm/pre-dispatch.js +2 -0
- package/dist/swarm/subagent-status-dir.d.ts +2 -1
- package/dist/swarm/subagent-status-dir.js +11 -2
- package/dist/swarm/worktrees.js +2 -0
- package/dist/verify-env/agent-hooks.d.ts +6 -1
- package/dist/verify-env/agent-hooks.js +28 -2
- 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
|
|
@@ -27,11 +27,51 @@ export type HookHostIdentitySource = {
|
|
|
27
27
|
readonly kind: "host-env";
|
|
28
28
|
readonly variable: string;
|
|
29
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[];
|
|
30
40
|
/** The identity source for a host, or null when the host has no contract. */
|
|
31
41
|
export declare function hookHostIdentitySource(host: string): HookHostIdentitySource | null;
|
|
32
42
|
/** Bound a raw host id by UTF-8 bytes, control characters and surrogate pairing. */
|
|
33
43
|
export declare function isUsableHostSessionId(raw: string): boolean;
|
|
34
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;
|
|
35
75
|
export type HostEnvIdentityStatus = "ok" | "missing" | "invalid";
|
|
36
76
|
export type HostEnvIdentityResolution = {
|
|
37
77
|
readonly status: "ok";
|
|
@@ -17,6 +17,28 @@ const HOST_IDENTITY_SOURCES = {
|
|
|
17
17
|
cursor: { kind: "payload", field: "conversation_id" },
|
|
18
18
|
grok: { kind: "host-env", variable: "GROK_SESSION_ID" },
|
|
19
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);
|
|
20
42
|
/** The identity source for a host, or null when the host has no contract. */
|
|
21
43
|
export function hookHostIdentitySource(host) {
|
|
22
44
|
return (HOST_IDENTITY_SOURCES[host] ?? null);
|
|
@@ -56,6 +78,46 @@ export function canonicalHostSessionId(provider, rawSessionId) {
|
|
|
56
78
|
const encoded = Buffer.from(rawSessionId, "utf8").toString("base64url");
|
|
57
79
|
return `host:${provider}:v1:${encoded}`;
|
|
58
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
|
+
}
|
|
59
121
|
/** Read one `host-env` provider's variable out of a process environment. */
|
|
60
122
|
export function readHostEnvIdentity(environ, variable) {
|
|
61
123
|
const raw = environ[variable];
|
|
@@ -76,11 +138,8 @@ export function readHostEnvIdentity(environ, variable) {
|
|
|
76
138
|
*/
|
|
77
139
|
export function ambientHostSessionOwner(environ = process.env) {
|
|
78
140
|
const resolved = [];
|
|
79
|
-
for (const provider of
|
|
80
|
-
const
|
|
81
|
-
if (source.kind !== "host-env")
|
|
82
|
-
continue;
|
|
83
|
-
const value = readHostEnvIdentity(environ, source.variable);
|
|
141
|
+
for (const { provider, variable } of HOST_ENV_IDENTITY_ENTRIES) {
|
|
142
|
+
const value = readHostEnvIdentity(environ, variable);
|
|
84
143
|
if (value.status === "ok")
|
|
85
144
|
resolved.push(canonicalHostSessionId(provider, value.rawSessionId));
|
|
86
145
|
}
|
package/dist/session/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
package/dist/session/index.js
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
@@ -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
|
|
@@ -237,20 +256,78 @@ export declare function formatOccupancyRemediation(record: OccupancyRecord, now?
|
|
|
237
256
|
* derives from. A grant admits writes; the lease has one owner.
|
|
238
257
|
*/
|
|
239
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;
|
|
240
295
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
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.
|
|
243
305
|
*
|
|
244
306
|
* The host step is what makes an identified host's claim reachable (#3873).
|
|
245
307
|
* Minting instead binds the lease to an id no later hook process can present,
|
|
246
308
|
* so the session that claimed the worktree is refused by its own lease. The
|
|
247
|
-
* mint stays as the last resort for hosts that publish nothing
|
|
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).
|
|
248
311
|
*/
|
|
249
312
|
export declare function resolveOccupancySessionId(input?: ApplyOccupancyInput): string;
|
|
250
313
|
export declare function readOccupancy(projectRoot: string): OccupancyRecord | null;
|
|
251
314
|
export declare function liveOccupant(projectRoot: string, now?: Date, ttlMs?: number, maxLeaseMs?: number): OccupancyRecord | null;
|
|
252
315
|
export declare function applyWorktreeOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
|
|
253
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
|
+
*/
|
|
254
331
|
export declare function releaseOccupancy(projectRoot: string, input?: {
|
|
255
332
|
readonly sessionId?: string;
|
|
256
333
|
readonly env?: NodeJS.ProcessEnv;
|
|
@@ -333,7 +410,15 @@ export declare function evaluateOccupancyWriteGate(projectRoot: string, input?:
|
|
|
333
410
|
* Never claims and never mints an owner — an unheld or foreign lease is denied.
|
|
334
411
|
*/
|
|
335
412
|
export declare function heartbeatOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
|
|
336
|
-
/**
|
|
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
|
+
*/
|
|
337
422
|
export declare function releaseSwarmOccupancy(projectRoot: string, input?: {
|
|
338
423
|
readonly sessionId?: string;
|
|
339
424
|
readonly env?: NodeJS.ProcessEnv;
|