@sema-agent/core 5.21.0 → 5.22.0
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/CHANGELOG.md +66 -0
- package/dist/agents/send-message-tool.js +6 -3
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +45 -4
- package/dist/brain/errors.d.ts +20 -0
- package/dist/brain/errors.js +40 -0
- package/dist/brain/retry.d.ts +16 -2
- package/dist/brain/retry.js +3 -2
- package/dist/brain/status-sink.d.ts +9 -2
- package/dist/brain/stream-engine.d.ts +22 -0
- package/dist/brain/stream-engine.js +41 -10
- package/dist/core/ask-class.d.ts +48 -0
- package/dist/core/ask-class.js +33 -0
- package/dist/core/checkpoint-store.d.ts +103 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/governance-codes.d.ts +38 -0
- package/dist/core/governance-codes.js +11 -0
- package/dist/core/hooks.d.ts +39 -0
- package/dist/core/hooks.js +26 -2
- package/dist/core/locked-config.d.ts +7 -1
- package/dist/core/locked-config.js +2 -1
- package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
- package/dist/core/memory-engine/delegation-provenance.js +26 -0
- package/dist/core/memory-engine/engine.d.ts +67 -1
- package/dist/core/memory-engine/engine.js +270 -12
- package/dist/core/memory-engine/header-hints.d.ts +30 -0
- package/dist/core/memory-engine/header-hints.js +41 -0
- package/dist/core/memory-engine/index.d.ts +3 -2
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +166 -0
- package/dist/core/memory-engine/layout.js +399 -0
- package/dist/core/memory-engine/tools.d.ts +30 -0
- package/dist/core/memory-engine/tools.js +108 -17
- package/dist/core/permission-rule-consent.d.ts +25 -9
- package/dist/core/permission-rule-consent.js +91 -20
- package/dist/core/permission-rule-model.d.ts +9 -1
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rule-org.d.ts +161 -0
- package/dist/core/permission-rule-org.js +211 -0
- package/dist/core/permission-rule-store.d.ts +249 -6
- package/dist/core/permission-rule-store.js +313 -3
- package/dist/core/permission-rule-sync.d.ts +131 -0
- package/dist/core/permission-rule-sync.js +314 -0
- package/dist/core/runner/prepare-memory.js +35 -8
- package/dist/core/runner/prepare-task.d.ts +54 -1
- package/dist/core/runner/prepare-task.js +246 -27
- package/dist/core/runner/runtask.js +147 -6
- package/dist/core/shared-memory/contract.js +19 -4
- package/dist/core/shared-memory/normalize.d.ts +3 -1
- package/dist/core/shared-memory/tools.js +73 -17
- package/dist/core/shared-memory/types.d.ts +27 -1
- package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
- package/dist/core/task-notification.d.ts +5 -2
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +6 -2
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry.d.ts +9 -3
- package/dist/core/task-registry.js +2 -0
- package/dist/core/tool-policy.d.ts +120 -2
- package/dist/core/tool-policy.js +116 -6
- package/dist/core/trace.d.ts +32 -1
- package/dist/core/types.d.ts +56 -3
- package/dist/index.d.ts +12 -7
- package/dist/index.js +10 -5
- package/dist/stores/file/checkpoint-store.d.ts +4 -0
- package/dist/stores/file/checkpoint-store.js +1 -0
- package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
- package/dist/stores/file/permission-rule-adopt.js +95 -0
- package/dist/stores/file/permission-rule-store.d.ts +80 -2
- package/dist/stores/file/permission-rule-store.js +189 -46
- package/dist/tools/fs/fs-search-tools.js +0 -1
- package/package.json +1 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { parseAllowRuleText } from "./permission-rule-model.js";
|
|
2
|
+
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameScope, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
3
|
+
export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
|
|
4
|
+
export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
|
|
5
|
+
function refuseLocalOwnerSync() {
|
|
6
|
+
const e = new Error("a local-owner rule bucket cannot sync — syncing is an authenticated act, and this bucket has no cloud home until it is adopted (adoptFilePermissionRuleStore); this is NOT an authentication failure");
|
|
7
|
+
e.code = LOCAL_OWNER_UNSYNCABLE_CODE;
|
|
8
|
+
throw e;
|
|
9
|
+
}
|
|
10
|
+
const SYNC_MAX_ATTEMPTS = 8;
|
|
11
|
+
export async function syncPermissionRules(opts) {
|
|
12
|
+
if (opts.owner !== undefined) {
|
|
13
|
+
if (opts.owner.kind === "local-owner")
|
|
14
|
+
refuseLocalOwnerSync();
|
|
15
|
+
if (opts.owner.principal !== opts.principal) {
|
|
16
|
+
throw new Error(`contradictory identity: principal "${opts.principal}" and owner principal "${opts.owner.principal}" disagree`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (typeof opts.principal !== "string" || opts.principal === "") {
|
|
20
|
+
throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
|
|
21
|
+
}
|
|
22
|
+
const store = opts.provider.forPrincipal(opts.principal);
|
|
23
|
+
const writer = writerOf(store);
|
|
24
|
+
if (writer === undefined) {
|
|
25
|
+
throw new Error("the resolved permission-rule store has no write face — a sync round cannot land through it");
|
|
26
|
+
}
|
|
27
|
+
const raw = await writer.readRaw();
|
|
28
|
+
const request = {
|
|
29
|
+
principal: opts.principal,
|
|
30
|
+
replica: raw.actor,
|
|
31
|
+
state: { rules: raw.rules, tombstones: raw.tombstones },
|
|
32
|
+
...(raw.observedVector !== undefined ? { observedVector: raw.observedVector } : {}),
|
|
33
|
+
};
|
|
34
|
+
const rawResponse = await opts.transport(PERMISSION_RULE_SYNC_PATH, request);
|
|
35
|
+
const response = parseRuleSyncResponse(rawResponse, opts.principal);
|
|
36
|
+
const warnings = [...response.warnings];
|
|
37
|
+
const dropped = [];
|
|
38
|
+
const inboundTombstones = [];
|
|
39
|
+
let tombstoneWithholding = false;
|
|
40
|
+
const forgedDot = (d) => d.actor === raw.actor && d.counter > raw.counter;
|
|
41
|
+
for (const t of response.merged.tombstones) {
|
|
42
|
+
if (t === undefined) {
|
|
43
|
+
tombstoneWithholding = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const forged = forgedDot(t.deletedBy) ? t.deletedBy : t.removedDots.find(forgedDot);
|
|
47
|
+
if (forged !== undefined) {
|
|
48
|
+
dropped.push({ rule: t.rule, scope: t.scope, dot: forged, reason: "own_actor_forged" });
|
|
49
|
+
tombstoneWithholding = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
inboundTombstones.push(t);
|
|
53
|
+
}
|
|
54
|
+
if (tombstoneWithholding) {
|
|
55
|
+
warnings.push("the response carried at least one malformed tombstone — every inbound add is withheld this round (legal tombstones land; the adds re-arrive once the peer repairs the tombstone: full-state exchange has no missed-forever window)");
|
|
56
|
+
}
|
|
57
|
+
const inboundRules = [];
|
|
58
|
+
if (!tombstoneWithholding) {
|
|
59
|
+
for (const r of response.merged.rules) {
|
|
60
|
+
const n = normalizePersistedRule(r);
|
|
61
|
+
if ("reject" in n) {
|
|
62
|
+
for (const a of r.adds)
|
|
63
|
+
dropped.push({ rule: r.rule, scope: r.scope, dot: a.dot, reason: n.reject });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const kept = r.adds.filter((a) => {
|
|
67
|
+
if (a.dot.actor === raw.actor && a.dot.counter > raw.counter) {
|
|
68
|
+
dropped.push({ rule: r.rule, scope: r.scope, dot: a.dot, reason: "own_actor_forged" });
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
if (kept.length > 0)
|
|
74
|
+
inboundRules.push({ ...r, adds: kept });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const inbound = { rules: inboundRules, tombstones: inboundTombstones };
|
|
78
|
+
const quarantine = [];
|
|
79
|
+
const localScreen = screenRuleSyncState({ rules: raw.rules, tombstones: raw.tombstones });
|
|
80
|
+
for (const rej of localScreen.rejected) {
|
|
81
|
+
if (rej.reason === "invalid_rule_text" || rej.reason === "metadata_mismatch") {
|
|
82
|
+
quarantine.push({ rule: rej.rule, scope: rej.scope, dots: [rej.dot], reason: rej.reason });
|
|
83
|
+
dropped.push(rej);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const localDots = new Set(raw.rules.flatMap((r) => r.adds.map((a) => JSON.stringify([a.dot.actor, a.dot.counter]))));
|
|
87
|
+
for (const d of response.dropped) {
|
|
88
|
+
if (localDots.has(JSON.stringify([d.dot.actor, d.dot.counter]))) {
|
|
89
|
+
const reason = d.reason === "below_gc_frontier" ? "below_gc_frontier" : "server_rejected";
|
|
90
|
+
quarantine.push({ rule: d.rule, scope: d.scope, dots: [d.dot], reason });
|
|
91
|
+
dropped.push({ rule: d.rule, scope: d.scope, dot: d.dot, reason });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
warnings.push(`the server dropped an inbound-side record (${d.rule}, dot ${d.dot.actor}#${d.dot.counter}): ${d.reason}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const clean = !tombstoneWithholding;
|
|
98
|
+
let current = raw;
|
|
99
|
+
for (let attempt = 0; attempt < SYNC_MAX_ATTEMPTS; attempt++) {
|
|
100
|
+
const preLive = applyTombstones(current.rules, current.tombstones);
|
|
101
|
+
const prospective = joinRuleStates({ rules: current.rules, tombstones: current.tombstones }, inbound);
|
|
102
|
+
const delta = {
|
|
103
|
+
kind: "sync-join",
|
|
104
|
+
inbound,
|
|
105
|
+
...(response.gcFrontier !== undefined ? { gcFrontier: response.gcFrontier } : {}),
|
|
106
|
+
...(clean ? { observedVector: joinFrontiers(current.observedVector ?? {}, ruleSyncVector(prospective)) } : {}),
|
|
107
|
+
...(quarantine.length > 0 ? { quarantine } : {}),
|
|
108
|
+
};
|
|
109
|
+
let res;
|
|
110
|
+
try {
|
|
111
|
+
res = await writer.apply(delta, { expectedRev: current.rev });
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
117
|
+
landed: { newAdds: 0, newTombstones: 0 },
|
|
118
|
+
resurrected: [],
|
|
119
|
+
dropped,
|
|
120
|
+
rev: current.rev,
|
|
121
|
+
warnings: [...warnings, `the store refused the sync landing: ${errText(err)} — nothing landed, the local state is unchanged`],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if ("conflict" in res) {
|
|
125
|
+
current = await writer.readRaw();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const landedRaw = await writer.readRaw();
|
|
129
|
+
const postLive = applyTombstones(landedRaw.rules, landedRaw.tombstones);
|
|
130
|
+
const resurrected = [];
|
|
131
|
+
for (const r of postLive) {
|
|
132
|
+
const pre = preLive.find((p) => p.rule === r.rule && sameScope(p.scope, r.scope));
|
|
133
|
+
if (pre !== undefined)
|
|
134
|
+
continue;
|
|
135
|
+
const hadAdds = current.rules.some((p) => p.rule === r.rule && sameScope(p.scope, r.scope));
|
|
136
|
+
const covered = current.tombstones.some((t) => t.rule === r.rule && sameScope(t.scope, r.scope));
|
|
137
|
+
if (hadAdds && covered) {
|
|
138
|
+
const preRuleDots = new Set(current.rules.filter((p) => p.rule === r.rule && sameScope(p.scope, r.scope)).flatMap((p) => p.adds.map((a) => JSON.stringify([a.dot.actor, a.dot.counter]))));
|
|
139
|
+
const by = r.adds.find((a) => !preRuleDots.has(JSON.stringify([a.dot.actor, a.dot.counter]))) ?? r.adds[0];
|
|
140
|
+
if (by !== undefined)
|
|
141
|
+
resurrected.push({ rule: r.rule, scope: r.scope, byDot: by.dot });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const dotKeyOf = (d) => JSON.stringify([d.actor, d.counter]);
|
|
145
|
+
const preDots = new Set(current.rules.flatMap((r) => r.adds.map((a) => dotKeyOf(a.dot))));
|
|
146
|
+
const newAdds = landedRaw.rules.flatMap((r) => r.adds).filter((a) => !preDots.has(dotKeyOf(a.dot))).length;
|
|
147
|
+
const tombKeyOf = (t) => JSON.stringify([t.rule, t.scope.kind === "global" ? null : t.scope.root, dotKeyOf(t.deletedBy)]);
|
|
148
|
+
const preTombs = new Set(current.tombstones.map(tombKeyOf));
|
|
149
|
+
const newTombstones = landedRaw.tombstones.filter((t) => !preTombs.has(tombKeyOf(t))).length;
|
|
150
|
+
const landedReport = res.sync;
|
|
151
|
+
for (const d of landedReport?.droppedInbound ?? []) {
|
|
152
|
+
if (!dropped.some((x) => x.dot.actor === d.dot.actor && x.dot.counter === d.dot.counter && x.reason === d.reason))
|
|
153
|
+
dropped.push(d);
|
|
154
|
+
}
|
|
155
|
+
for (const q of landedReport?.quarantined ?? []) {
|
|
156
|
+
if (!dropped.some((x) => x.dot.actor === q.dot.actor && x.dot.counter === q.dot.counter && x.reason === q.reason))
|
|
157
|
+
dropped.push(q);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
ok: dropped.length === 0 && warnings.length === 0,
|
|
161
|
+
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
162
|
+
landed: { newAdds, newTombstones },
|
|
163
|
+
resurrected,
|
|
164
|
+
dropped,
|
|
165
|
+
rev: landedRaw.rev,
|
|
166
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
172
|
+
landed: { newAdds: 0, newTombstones: 0 },
|
|
173
|
+
resurrected: [],
|
|
174
|
+
dropped,
|
|
175
|
+
rev: current.rev,
|
|
176
|
+
warnings: [...warnings, `optimistic-concurrency retries exhausted after ${SYNC_MAX_ATTEMPTS} attempts — nothing landed, the local state is unchanged`],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function isDot(v) {
|
|
180
|
+
const d = v;
|
|
181
|
+
return typeof d?.actor === "string" && d.actor !== "" && typeof d.counter === "number" && Number.isFinite(d.counter);
|
|
182
|
+
}
|
|
183
|
+
function pickScope(v) {
|
|
184
|
+
const s = v;
|
|
185
|
+
if (s?.kind === "global")
|
|
186
|
+
return { kind: "global" };
|
|
187
|
+
if (s?.kind === "project" && typeof s.root === "string" && s.root !== "")
|
|
188
|
+
return { kind: "project", root: s.root };
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function pickAdd(v) {
|
|
192
|
+
const a = v;
|
|
193
|
+
if (!isDot(a?.dot))
|
|
194
|
+
return undefined;
|
|
195
|
+
if (a.origin !== "user" && a.origin !== "imported-cc" && a.origin !== "starter")
|
|
196
|
+
return undefined;
|
|
197
|
+
if (typeof a.createdAt !== "string")
|
|
198
|
+
return undefined;
|
|
199
|
+
return { dot: { actor: a.dot.actor, counter: a.dot.counter }, origin: a.origin, createdAt: a.createdAt };
|
|
200
|
+
}
|
|
201
|
+
function pickFrontier(v) {
|
|
202
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
203
|
+
return undefined;
|
|
204
|
+
const out = {};
|
|
205
|
+
for (const [actor, counter] of Object.entries(v)) {
|
|
206
|
+
if (typeof counter !== "number" || !Number.isFinite(counter))
|
|
207
|
+
return undefined;
|
|
208
|
+
out[actor] = counter;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
213
|
+
if (raw === null || typeof raw !== "object") {
|
|
214
|
+
throw new Error("the sync response is not an object — protocol error, nothing landed");
|
|
215
|
+
}
|
|
216
|
+
const r = raw;
|
|
217
|
+
if (typeof r.principal !== "string") {
|
|
218
|
+
throw new Error("the sync response carries no principal echo — protocol error, nothing landed");
|
|
219
|
+
}
|
|
220
|
+
if (r.principal !== expectedPrincipal) {
|
|
221
|
+
throw new Error(`the sync response echoes principal "${r.principal}" but this round synced "${expectedPrincipal}" — the transport is wired to the wrong bucket; refusing the whole round`);
|
|
222
|
+
}
|
|
223
|
+
const merged = r.merged;
|
|
224
|
+
if (merged === null || typeof merged !== "object" || !Array.isArray(merged.rules) || !Array.isArray(merged.tombstones)) {
|
|
225
|
+
throw new Error("the sync response carries no merged state — protocol error, nothing landed");
|
|
226
|
+
}
|
|
227
|
+
let gcFrontier;
|
|
228
|
+
if (r.gcFrontier !== undefined) {
|
|
229
|
+
gcFrontier = pickFrontier(r.gcFrontier);
|
|
230
|
+
if (gcFrontier === undefined) {
|
|
231
|
+
throw new Error("the sync response carries an unparseable gcFrontier — refusing the whole round (a garbled frontier must not drive the fence arm)");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const warnings = [];
|
|
235
|
+
const rules = [];
|
|
236
|
+
for (const entry of merged.rules) {
|
|
237
|
+
const e = entry;
|
|
238
|
+
const scope = pickScope(e?.scope);
|
|
239
|
+
if (typeof e?.rule !== "string" || scope === undefined || !Array.isArray(e.adds) || typeof e.command !== "string" || typeof e.tool !== "string" || typeof e.match !== "string") {
|
|
240
|
+
warnings.push("the response carried an unshapeable rule entry — skipped (it never reaches the local store)");
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const adds = [];
|
|
244
|
+
let addDamaged = false;
|
|
245
|
+
for (const a of e.adds) {
|
|
246
|
+
const picked = pickAdd(a);
|
|
247
|
+
if (picked === undefined) {
|
|
248
|
+
addDamaged = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
adds.push(picked);
|
|
252
|
+
}
|
|
253
|
+
if (addDamaged)
|
|
254
|
+
warnings.push(`rule "${e.rule}" carried at least one unshapeable add — skipped those adds`);
|
|
255
|
+
if (adds.length === 0)
|
|
256
|
+
continue;
|
|
257
|
+
rules.push({
|
|
258
|
+
rule: e.rule,
|
|
259
|
+
tool: e.tool,
|
|
260
|
+
match: e.match,
|
|
261
|
+
command: e.command,
|
|
262
|
+
scope,
|
|
263
|
+
adds,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const tombstones = [];
|
|
267
|
+
for (const entry of merged.tombstones) {
|
|
268
|
+
const t = entry;
|
|
269
|
+
const scope = pickScope(t?.scope);
|
|
270
|
+
const canonicalTomb = typeof t?.rule === "string" ? parseAllowRuleText(t.rule) : undefined;
|
|
271
|
+
if (typeof t?.rule !== "string" ||
|
|
272
|
+
canonicalTomb === undefined ||
|
|
273
|
+
"reject" in canonicalTomb ||
|
|
274
|
+
canonicalTomb.rule.rule !== t.rule ||
|
|
275
|
+
scope === undefined ||
|
|
276
|
+
!isDot(t.deletedBy) ||
|
|
277
|
+
!Array.isArray(t.removedDots) ||
|
|
278
|
+
t.removedDots.length === 0 ||
|
|
279
|
+
!t.removedDots.every(isDot)) {
|
|
280
|
+
tombstones.push(undefined);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
tombstones.push({
|
|
284
|
+
rule: t.rule,
|
|
285
|
+
scope,
|
|
286
|
+
removedDots: t.removedDots.map((d) => ({ actor: d.actor, counter: d.counter })),
|
|
287
|
+
deletedBy: { actor: t.deletedBy.actor, counter: t.deletedBy.counter },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const dropped = [];
|
|
291
|
+
if (r.dropped !== undefined) {
|
|
292
|
+
if (!Array.isArray(r.dropped)) {
|
|
293
|
+
warnings.push("the response carried a non-array dropped field — ignored");
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
for (const entry of r.dropped) {
|
|
297
|
+
const d = entry;
|
|
298
|
+
const scope = pickScope(d?.scope);
|
|
299
|
+
if (typeof d?.rule !== "string" || scope === undefined || !isDot(d.dot) || typeof d.reason !== "string") {
|
|
300
|
+
warnings.push("the response carried an unshapeable dropped entry — ignored (the server's own drop discipline is its contract obligation)");
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
dropped.push({ rule: d.rule, scope, dot: { actor: d.dot.actor, counter: d.dot.counter }, reason: d.reason });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
principal: r.principal,
|
|
309
|
+
merged: { rules, tombstones },
|
|
310
|
+
...(gcFrontier !== undefined ? { gcFrontier } : {}),
|
|
311
|
+
dropped,
|
|
312
|
+
warnings,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -2,7 +2,7 @@ import { admitMemoryScopes } from "../memory-admission.js";
|
|
|
2
2
|
import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
3
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
4
4
|
import { normalizeMemorySpec } from "../memory.js";
|
|
5
|
-
import { MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
5
|
+
import { MEMORY_PREFERENCE_DISCIPLINE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
6
|
import { createMemoryEngineTools } from "../memory-engine/tools.js";
|
|
7
7
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
8
8
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
@@ -151,13 +151,23 @@ export async function prepareMemory(input) {
|
|
|
151
151
|
writeHandle = writeIsPersonal ? personalHandle : projectHandle;
|
|
152
152
|
injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
|
|
153
153
|
toolPlanes = [
|
|
154
|
-
{
|
|
155
|
-
|
|
154
|
+
{
|
|
155
|
+
backend: retrievalBackend(backend),
|
|
156
|
+
scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null),
|
|
157
|
+
recordRetrieved: (ids) => projectEngine.recordRetrieved(ids),
|
|
158
|
+
challengeExclusions: () => projectEngine.readChallengeExclusions(),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
backend: retrievalBackend(personal.backend),
|
|
162
|
+
scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null),
|
|
163
|
+
recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
|
|
164
|
+
challengeExclusions: () => personalEngine.readChallengeExclusions(),
|
|
165
|
+
},
|
|
156
166
|
];
|
|
157
167
|
harvestBoth = async () => {
|
|
158
168
|
const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
|
|
159
169
|
const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
|
|
160
|
-
const writeReport = await writeFirst[0].harvest(writeFirst[1], pollutedOpts(writeFirst[0]));
|
|
170
|
+
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId });
|
|
161
171
|
let readReport;
|
|
162
172
|
let readFailure;
|
|
163
173
|
try {
|
|
@@ -179,8 +189,15 @@ export async function prepareMemory(input) {
|
|
|
179
189
|
writeEngine = personalEngine;
|
|
180
190
|
writeHandle = handle;
|
|
181
191
|
injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
182
|
-
harvestBoth = () => personalEngine.harvest(handle, pollutedOpts(personalEngine));
|
|
183
|
-
toolPlanes = [
|
|
192
|
+
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId });
|
|
193
|
+
toolPlanes = [
|
|
194
|
+
{
|
|
195
|
+
backend: retrievalBackend(personal.backend),
|
|
196
|
+
scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
|
|
197
|
+
recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
|
|
198
|
+
challengeExclusions: () => personalEngine.readChallengeExclusions(),
|
|
199
|
+
},
|
|
200
|
+
];
|
|
184
201
|
}
|
|
185
202
|
else {
|
|
186
203
|
const engine = new MemoryEngine({
|
|
@@ -193,8 +210,15 @@ export async function prepareMemory(input) {
|
|
|
193
210
|
writeEngine = engine;
|
|
194
211
|
writeHandle = handle;
|
|
195
212
|
injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
196
|
-
harvestBoth = () => engine.harvest(handle, pollutedOpts(engine));
|
|
197
|
-
toolPlanes = [
|
|
213
|
+
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId });
|
|
214
|
+
toolPlanes = [
|
|
215
|
+
{
|
|
216
|
+
backend: retrievalBackend(backend),
|
|
217
|
+
scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
|
|
218
|
+
recordRetrieved: (ids) => engine.recordRetrieved(ids),
|
|
219
|
+
challengeExclusions: () => engine.readChallengeExclusions(),
|
|
220
|
+
},
|
|
221
|
+
];
|
|
198
222
|
}
|
|
199
223
|
memoryWriteGateRef.current = (w) => writeEngine.gateWrite(writeHandle, w.key, w.content);
|
|
200
224
|
const harvestSafe = async (phase = "terminal") => {
|
|
@@ -256,6 +280,9 @@ export async function prepareMemory(input) {
|
|
|
256
280
|
if (memoryTools !== undefined) {
|
|
257
281
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_RECALL_DISCIPLINE}` : MEMORY_RECALL_DISCIPLINE;
|
|
258
282
|
}
|
|
283
|
+
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted) {
|
|
284
|
+
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
|
|
285
|
+
}
|
|
259
286
|
if (memoryBlock !== undefined && injection.indexSeed !== undefined)
|
|
260
287
|
seedFiles = [injection.indexSeed];
|
|
261
288
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AgentHarness, type ThinkingLevel } from "../../internal/harness.js";
|
|
2
2
|
import type { Model } from "../../internal/llm.js";
|
|
3
3
|
import { type CompactionForkContext } from "../auto-compaction.js";
|
|
4
|
+
import { type AutoModeDecider } from "../auto-mode.js";
|
|
4
5
|
import { type MaterializedMcp } from "../mcp.js";
|
|
5
6
|
import { type MaterializedA2a } from "../a2a.js";
|
|
6
7
|
import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
|
|
@@ -869,7 +870,20 @@ export interface InheritedGate {
|
|
|
869
870
|
orgAdmissionGoverned?: true;
|
|
870
871
|
/** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
|
|
871
872
|
* RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
|
|
872
|
-
* enforces, so a deps-level baseline is inherited too).
|
|
873
|
+
* enforces, so a deps-level baseline is inherited too).
|
|
874
|
+
*
|
|
875
|
+
* ⚖️ A-005.13 (ruled with the F-012 family, 2026-08-09): the ancestor's per-run BUDGET LEDGER does
|
|
876
|
+
* NOT travel this chain — by design, not by omission. An inherited policy's `check` receives the
|
|
877
|
+
* CHILD run's own `ToolCallRequest.budget` snapshot (the child's durable resource ledger), so a
|
|
878
|
+
* SELF-LIMITING ancestor policy ("allow N escalations, then ask") sees a fresh ledger in each
|
|
879
|
+
* delegated child rather than a continuation of the ancestor's counts. Freezing/snapshotting the
|
|
880
|
+
* ancestor ledger was considered and rejected: a frozen ledger goes stale the moment the ancestor
|
|
881
|
+
* keeps running (a stale ledger is a NEW wrongness surface, not a fix), and the durable resource
|
|
882
|
+
* ledger's identity is per-run by contract. A deployment wanting cross-delegation budget coherence
|
|
883
|
+
* carries it in its own policy state (the closure travels the chain intact — a closure-counter
|
|
884
|
+
* policy DOES aggregate across the tree, since every layer evaluates the same instance).
|
|
885
|
+
*
|
|
886
|
+
* `onAsk` is the ancestor's frozen
|
|
873
887
|
* `spec.onAsk ?? deps.onAsk` — an ancestor `ask` resolves at that ancestor's own approver
|
|
874
888
|
* (headless ⇒ deny), never at the child's. `durableMandate` is frozen at chain-assembly time when the
|
|
875
889
|
* ancestor ran under a durable-approval regime that would actually PARK its own plain asks — a
|
|
@@ -883,6 +897,29 @@ export interface InheritedGate {
|
|
|
883
897
|
policy: ToolPolicy;
|
|
884
898
|
onAsk?: OnAsk;
|
|
885
899
|
durableMandate?: boolean;
|
|
900
|
+
/** The CONTENT-ask twin of `durableMandate`, frozen from the QUESTION seat (`spec.onQuestion ??
|
|
901
|
+
* deps.onQuestion`) on the same rule: a `forceDurableGate` entitlement, or `durableApproval` with no
|
|
902
|
+
* LIVE question face frozen (absent, or the reserved resume placeholder — neither can answer). A
|
|
903
|
+
* wrapper seeing an `ask` on the question tool consults THIS bit, never the permission one: the two
|
|
904
|
+
* asks are answered by two different faces, and a run with a live `onQuestion` and no permission
|
|
905
|
+
* approver can answer a delegated question perfectly well. When it IS set the delegated question is
|
|
906
|
+
* marked unresolvable and takes the child gate's park / honest-refusal leg, exactly as an inherited
|
|
907
|
+
* permission ask does under `durableMandate`. */
|
|
908
|
+
contentMandate?: boolean;
|
|
909
|
+
/**
|
|
910
|
+
* F-012 (#106) — the ancestor's FROZEN auto-mode classifier: the same decider instance the
|
|
911
|
+
* ancestor's own gate consults for a surviving ask (`ToolGateInput.autoMode`). Without it the
|
|
912
|
+
* ancestor's ask DECISION CHAIN did not travel — the wrappers resolved an inherited ask straight
|
|
913
|
+
* at the frozen approver, so an ask the ancestor's classifier would have BLOCKED executed in the
|
|
914
|
+
* child on the frozen approver's allow. The wrapper runs it BEFORE the frozen approver, in the
|
|
915
|
+
* ancestor's own gate order (classifier → approver): `block` ⇒ deny (`decisionReason:"classifier"`),
|
|
916
|
+
* `allow` ⇒ the classifier's auto-allow (the frozen approver is not consulted), anything else
|
|
917
|
+
* falls through to the approver chain. Never consulted for the reserved question tool nor under a
|
|
918
|
+
* durable mandate (a marked call must reach the park with no synchronous decision-maker between).
|
|
919
|
+
*/
|
|
920
|
+
autoMode?: {
|
|
921
|
+
decider: AutoModeDecider;
|
|
922
|
+
};
|
|
886
923
|
/**
|
|
887
924
|
* Set ONLY on an entry whose `policy` is the folded form of an ancestor's PreToolUse screening face
|
|
888
925
|
* (issue #33 — the face used to stop at the task that installed it, so a call the ancestor had
|
|
@@ -1022,6 +1059,22 @@ export interface RunInternals {
|
|
|
1022
1059
|
* Absent ⇒ no inheritance (a top-level task; fully backward-compatible).
|
|
1023
1060
|
*/
|
|
1024
1061
|
inheritedGate?: InheritedGate;
|
|
1062
|
+
/**
|
|
1063
|
+
* design/180 half A — the delegation RUNTIME-PROVENANCE channel. Minted by the spawning delegation
|
|
1064
|
+
* tool when the parent chain is armed (the parent mounts a memory session, or is itself recording
|
|
1065
|
+
* for ITS parent): `ref` is the child's monotonic aggregate — the child's recorder writes into it
|
|
1066
|
+
* as the run progresses (live faces read the current bits; the terminal attestation reduces from
|
|
1067
|
+
* it); `contentSafety` is the chain's FROZEN classification snapshot — the child may narrow it
|
|
1068
|
+
* with its own config, never widen (design/180 A-2). Trusted internals chain only, same posture as
|
|
1069
|
+
* {@link inheritedGate}. Absent ⇒ the child records nothing (its deliveries then read `unknown`,
|
|
1070
|
+
* and every judgment falls back to the static floor — fail-closed by construction).
|
|
1071
|
+
*/
|
|
1072
|
+
delegationProvenance?: {
|
|
1073
|
+
ref: {
|
|
1074
|
+
current: import("../memory-engine/delegation-provenance.js").DelegationProvenanceAggregate;
|
|
1075
|
+
};
|
|
1076
|
+
contentSafety: import("../memory-engine/delegation-provenance.js").DelegationContentSafety;
|
|
1077
|
+
};
|
|
1025
1078
|
/**
|
|
1026
1079
|
* #22 (ruled 2026-08-05) — the SESSION-scoped org-admission freeze for a SAME-SESSION continuation
|
|
1027
1080
|
* that is NOT a checkpoint resume: a retained background child revived through the in-process resume
|