@sema-agent/core 5.22.0 → 5.23.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 +47 -0
- package/dist/agents/subagent.js +3 -2
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +62 -1
- package/dist/core/hooks.js +90 -12
- package/dist/core/memory-engine/engine.d.ts +28 -1
- package/dist/core/memory-engine/engine.js +62 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +69 -3
- package/dist/core/memory-engine/layout.js +75 -6
- package/dist/core/permission-rule-consent.js +2 -1
- package/dist/core/permission-rule-org.d.ts +36 -2
- package/dist/core/permission-rule-org.js +23 -0
- package/dist/core/permission-rule-store.js +2 -1
- package/dist/core/permission-rule-sync.d.ts +8 -0
- package/dist/core/permission-rule-sync.js +35 -6
- package/dist/core/runner/prepare-task.d.ts +10 -2
- package/dist/core/runner/prepare-task.js +111 -5
- package/dist/core/runner/runtask.js +19 -0
- package/dist/core/tool-policy.d.ts +37 -4
- package/dist/core/tool-policy.js +32 -4
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +47 -0
- package/dist/core/types.d.ts +38 -0
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.js +2 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/session-policy-store.js +2 -0
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/package.json +3 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, constants as fsConstants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
1
|
+
import { closeSync, constants as fsConstants, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
2
|
const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW } = fsConstants;
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -984,9 +984,10 @@ function coerceChallenges(raw) {
|
|
|
984
984
|
}
|
|
985
985
|
return raw;
|
|
986
986
|
}
|
|
987
|
+
export const CHALLENGE_LEDGER_MAX_EVENTS = 10_000;
|
|
987
988
|
export function appendChallengeEvents(controlDir, events, now) {
|
|
988
989
|
if (events.length === 0)
|
|
989
|
-
return [];
|
|
990
|
+
return { assignments: [], eventCount: undefined };
|
|
990
991
|
return lockedStrictUpdate(controlDir, CHALLENGES_FILE, "memory challenge ledger", coerceChallenges, (rec) => {
|
|
991
992
|
const byEventId = new Map(rec.events.map((e) => [e.eventId, e]));
|
|
992
993
|
const maxGen = new Map();
|
|
@@ -1020,17 +1021,17 @@ export function appendChallengeEvents(controlDir, events, now) {
|
|
|
1020
1021
|
out.push({ entryId: ev.entryId, eventId: ev.eventId, generation, at: ev.at, replayed: false });
|
|
1021
1022
|
changed = true;
|
|
1022
1023
|
}
|
|
1023
|
-
return { ...(changed ? { next: rec } : {}), result: out };
|
|
1024
|
+
return { ...(changed ? { next: rec } : {}), result: { assignments: out, eventCount: rec.events.length } };
|
|
1024
1025
|
});
|
|
1025
1026
|
}
|
|
1026
1027
|
export function resolveChallengeEvent(controlDir, entryId, generation, reason, now, requestId) {
|
|
1027
1028
|
return lockedStrictUpdate(controlDir, CHALLENGES_FILE, "memory challenge ledger", coerceChallenges, (rec) => {
|
|
1028
1029
|
const opened = rec.events.some((e) => e.kind === "challenge" && e.entryId === entryId && e.generation === generation);
|
|
1029
1030
|
if (!opened)
|
|
1030
|
-
return { result: false };
|
|
1031
|
+
return { result: { resolved: false, eventCount: undefined } };
|
|
1031
1032
|
const alreadyResolved = rec.events.some((e) => e.kind === "resolve" && e.entryId === entryId && e.generation === generation);
|
|
1032
1033
|
if (alreadyResolved)
|
|
1033
|
-
return { result: true };
|
|
1034
|
+
return { result: { resolved: true, eventCount: undefined } };
|
|
1034
1035
|
rec.events.push({
|
|
1035
1036
|
eventId: requestId !== undefined && requestId !== "" ? `${requestId}:${entryId}` : `resolve:${entryId}:${generation}`,
|
|
1036
1037
|
entryId,
|
|
@@ -1039,7 +1040,7 @@ export function resolveChallengeEvent(controlDir, entryId, generation, reason, n
|
|
|
1039
1040
|
at: now(),
|
|
1040
1041
|
reason,
|
|
1041
1042
|
});
|
|
1042
|
-
return { next: rec, result: true };
|
|
1043
|
+
return { next: rec, result: { resolved: true, eventCount: rec.events.length } };
|
|
1043
1044
|
});
|
|
1044
1045
|
}
|
|
1045
1046
|
export function challengedEntryIds(controlDir) {
|
|
@@ -1061,6 +1062,74 @@ export function challengedEntryIds(controlDir) {
|
|
|
1061
1062
|
export function readChallengeEvents(controlDir) {
|
|
1062
1063
|
return coerceChallenges(readStrictSidecar(controlDir, CHALLENGES_FILE, "memory challenge ledger")).events;
|
|
1063
1064
|
}
|
|
1065
|
+
export function isStrictControlPlaneLedgerCorrupt(controlDir, ledger) {
|
|
1066
|
+
const fileName = ledger === "lineage" ? LINEAGE_FILE : CHALLENGES_FILE;
|
|
1067
|
+
const what = ledger === "lineage" ? "memory lineage ledger" : "memory challenge ledger";
|
|
1068
|
+
const coerce = ledger === "lineage" ? coerceLineage : coerceChallenges;
|
|
1069
|
+
try {
|
|
1070
|
+
coerce(readStrictSidecar(controlDir, fileName, what));
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
catch (err) {
|
|
1074
|
+
if (err instanceof ControlPlaneCorruptError)
|
|
1075
|
+
return true;
|
|
1076
|
+
throw err;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
|
|
1080
|
+
const fileName = ledger === "lineage" ? LINEAGE_FILE : CHALLENGES_FILE;
|
|
1081
|
+
const what = ledger === "lineage" ? "memory lineage ledger" : "memory challenge ledger";
|
|
1082
|
+
const coerce = ledger === "lineage" ? coerceLineage : coerceChallenges;
|
|
1083
|
+
const empty = ledger === "lineage" ? { version: 1, committed: {}, pending: {} } : { version: 1, events: [] };
|
|
1084
|
+
ensureDirExists(controlDir);
|
|
1085
|
+
const file = join(controlDir, fileName);
|
|
1086
|
+
const journal = `${file}.journal`;
|
|
1087
|
+
const lock = `${file}.lock`;
|
|
1088
|
+
const token = acquireSidecarLock(lock, { onDeadline: "throw" });
|
|
1089
|
+
try {
|
|
1090
|
+
let healthy = false;
|
|
1091
|
+
try {
|
|
1092
|
+
coerce(readStrictSidecar(controlDir, fileName, what));
|
|
1093
|
+
healthy = true;
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
if (!(err instanceof ControlPlaneCorruptError))
|
|
1097
|
+
throw err;
|
|
1098
|
+
}
|
|
1099
|
+
if (healthy) {
|
|
1100
|
+
const e = new Error(`${what} reads cleanly — refusing to rebuild it. This call exists to recover a ledger whose corruption already refuses every read; resetting a healthy one would drop taint evidence that is still doing its job.`);
|
|
1101
|
+
e.code = "memory.control_plane_not_corrupt";
|
|
1102
|
+
throw e;
|
|
1103
|
+
}
|
|
1104
|
+
const at = now();
|
|
1105
|
+
const quarantinedTo = [];
|
|
1106
|
+
assertSidecarLockOwnership(lock, token, what);
|
|
1107
|
+
for (const path of [file, journal]) {
|
|
1108
|
+
if (!existsSync(path))
|
|
1109
|
+
continue;
|
|
1110
|
+
let dest = `${path}.corrupt-${at}`;
|
|
1111
|
+
for (let n = 2;; n++) {
|
|
1112
|
+
try {
|
|
1113
|
+
copyFileSync(path, dest, fsConstants.COPYFILE_EXCL);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
if (err.code !== "EEXIST")
|
|
1118
|
+
throw err;
|
|
1119
|
+
dest = `${path}.corrupt-${at}-${n}`;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
quarantinedTo.push(dest);
|
|
1123
|
+
}
|
|
1124
|
+
assertSidecarLockOwnership(lock, token, what);
|
|
1125
|
+
atomicWriteFileSync(file, `${JSON.stringify(empty, null, 2)}\n`);
|
|
1126
|
+
rmSync(journal, { force: true });
|
|
1127
|
+
return { ledger, quarantinedTo, at };
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
releaseSidecarLock(lock, token);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1064
1133
|
function coerceChallengedHistory(raw) {
|
|
1065
1134
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1066
1135
|
return {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { parseAllowRuleText, suggestRulesForCommand } from "./permission-rule-model.js";
|
|
2
3
|
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
3
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
@@ -28,7 +29,7 @@ function nowIso(deps) {
|
|
|
28
29
|
function mintId(deps, prefix) {
|
|
29
30
|
if (deps.newId !== undefined)
|
|
30
31
|
return deps.newId();
|
|
31
|
-
return `${prefix}_${Date.now().toString(36)}_${
|
|
32
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes(9).toString("base64url")}`;
|
|
32
33
|
}
|
|
33
34
|
function requirePrincipal(principal, entry) {
|
|
34
35
|
if (typeof principal !== "string" || principal === "") {
|
|
@@ -80,8 +80,42 @@ export interface OrgRuleStatePersistence {
|
|
|
80
80
|
/** Duck-typed discovery of the persistence face on a store backend, `writerOf` style. */
|
|
81
81
|
export declare function orgRuleStatePersistenceOf(store: PermissionRuleStore): OrgRuleStatePersistence | undefined;
|
|
82
82
|
/** The `decisionReason` the consuming gate stamps on the synthetic asks it mints while org adjudication
|
|
83
|
-
* is unavailable. Exported so the gate integration and its tests share one spelling
|
|
84
|
-
|
|
83
|
+
* is unavailable. Exported so the gate integration and its tests share one spelling — and TYPED as the
|
|
84
|
+
* member of {@link import("./tool-policy.js").DecisionReason} it must equal, so the constant and the
|
|
85
|
+
* vocabulary cannot drift into two spellings of one word. */
|
|
86
|
+
export declare const ORG_UNAVAILABLE_DECISION_REASON: Extract<import("./tool-policy.js").DecisionReason, "org_unavailable">;
|
|
87
|
+
/**
|
|
88
|
+
* How long a single org adjudication may take before the consuming gate stops waiting and reads the
|
|
89
|
+
* answer as UNAVAILABLE (its fail-closed word).
|
|
90
|
+
*
|
|
91
|
+
* A bound is MANDATORY rather than optional because of where this call sits: on the hot path of every
|
|
92
|
+
* governed tool call, and — on the durable resume leg — after the checkpoint has been consumed and the
|
|
93
|
+
* start frame emitted. A provider that never settles would otherwise wedge the call past any task
|
|
94
|
+
* deadline (tasks carry no walltime by default), leaving the frame unpaired and the approval spent with
|
|
95
|
+
* nothing retryable. A hang is the one outcome worse than either verdict.
|
|
96
|
+
*
|
|
97
|
+
* The value matches the auto-mode classifier's per-round-trip cap — the other model/network call the
|
|
98
|
+
* permission path makes — because the reasoning is the same: a governance lookup that takes longer than
|
|
99
|
+
* this is indistinguishable from an outage, and an outage is exactly what the unavailable arm is for.
|
|
100
|
+
* The task's own abort signal still applies in parallel; whichever fires first ends the wait.
|
|
101
|
+
*/
|
|
102
|
+
export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
|
|
103
|
+
/**
|
|
104
|
+
* Await `p`, but settle with `fallback` if the deadline elapses or the signal fires first.
|
|
105
|
+
*
|
|
106
|
+
* Shared by both org consumption sites so one bound cannot drift into two. Deliberately NOT `.unref()`
|
|
107
|
+
* on the timer: this is a foreground rescue timer, and the caller is awaiting it inside the control
|
|
108
|
+
* flow — an unref'd timer stops firing in exactly the situation it exists for (an otherwise idle loop).
|
|
109
|
+
* A rejecting `p` also lands on the fallback: the callers' fail-closed word is the same either way, and
|
|
110
|
+
* an escaping rejection here would turn a dependency failure into a task crash.
|
|
111
|
+
*/
|
|
112
|
+
export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
timeoutMs: number;
|
|
115
|
+
}): Promise<T>;
|
|
116
|
+
/** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
|
|
117
|
+
* single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */
|
|
118
|
+
export declare const ORG_RULE_DECISION_REASON: Extract<import("./tool-policy.js").DecisionReason, "org_rule">;
|
|
85
119
|
/** How far into the future a snapshot's `fetchedAtMs` may sit before it is refused — ordinary NTP-level
|
|
86
120
|
* clock skew passes; a far-future timestamp (which would satisfy the staleness bound INDEFINITELY,
|
|
87
121
|
* turning fail-closed into evergreen freshness) does not. */
|
|
@@ -7,6 +7,29 @@ export function orgRuleStatePersistenceOf(store) {
|
|
|
7
7
|
: undefined;
|
|
8
8
|
}
|
|
9
9
|
export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
|
|
10
|
+
export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
11
|
+
export function settleOrgVerdictWithin(p, fallback, opts) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const finish = (v) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
20
|
+
resolve(v);
|
|
21
|
+
};
|
|
22
|
+
const onAbort = () => finish(fallback);
|
|
23
|
+
const timer = setTimeout(() => finish(fallback), opts.timeoutMs);
|
|
24
|
+
if (opts.signal?.aborted === true) {
|
|
25
|
+
finish(fallback);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
29
|
+
p.then(finish, () => finish(fallback));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export const ORG_RULE_DECISION_REASON = "org_rule";
|
|
10
33
|
export const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS = 5 * 60_000;
|
|
11
34
|
export function createOrgRuleOverlay(cfg) {
|
|
12
35
|
if (cfg.governed !== true) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { canonicalize } from "./canonical-json.js";
|
|
2
3
|
import { isRuleLive, parseAllowRuleText } from "./permission-rule-model.js";
|
|
3
4
|
export function sameRuleOwner(a, b) {
|
|
@@ -396,7 +397,7 @@ export class InMemoryPermissionRuleStore {
|
|
|
396
397
|
observedVector;
|
|
397
398
|
rev = 0;
|
|
398
399
|
counter = 0;
|
|
399
|
-
constructor(actor = `mem-${
|
|
400
|
+
constructor(actor = `mem-${randomBytes(6).toString("base64url")}`, now = Date.now) {
|
|
400
401
|
this.actor = actor;
|
|
401
402
|
this.now = now;
|
|
402
403
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import type { PersistedAllowRule, RuleDot, RuleScope, RuleTombstone } from "./permission-rule-model.js";
|
|
29
29
|
import type { PermissionRuleStoreProvider, RuleOwner, RuleSyncDrop, RuleSyncFrontier, RuleSyncState } from "./permission-rule-store.js";
|
|
30
|
+
import { type TracerHook } from "./trace.js";
|
|
30
31
|
/** The injected HTTP seam — core never bundles a fetch. The deployment owns base URL, auth, TLS and
|
|
31
32
|
* retries; a non-2xx / network failure should THROW (the round then aborts with zero local effects —
|
|
32
33
|
* the transport runs before any local write). */
|
|
@@ -103,6 +104,13 @@ export declare function syncPermissionRules(opts: {
|
|
|
103
104
|
transport: PermissionRuleSyncTransport;
|
|
104
105
|
/** Injected clock — observation timestamps only, never adjudication input. */
|
|
105
106
|
now?: () => number;
|
|
107
|
+
/** design/182 §4.6 — optional trace sink for the round's DISCLOSURES: one
|
|
108
|
+
* `permission.rule_sync_resurrected` per removed→live transition and one
|
|
109
|
+
* `permission.rule_sync_dropped` per refused/quarantined row. The same facts are already on the
|
|
110
|
+
* returned result; this is the operator channel for a deployment that drives sync outside a task
|
|
111
|
+
* (where there is no `RunnerDeps.tracer` in scope) and does not read the result itself. Absent ⇒ no
|
|
112
|
+
* events, and the round is byte-identical. */
|
|
113
|
+
tracer?: TracerHook;
|
|
106
114
|
}): Promise<PermissionRuleSyncResult>;
|
|
107
115
|
interface ParsedRuleSyncResponse {
|
|
108
116
|
principal: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseAllowRuleText } from "./permission-rule-model.js";
|
|
2
2
|
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameScope, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
3
|
+
import { emitTrace } from "./trace.js";
|
|
3
4
|
export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
|
|
4
5
|
export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
|
|
5
6
|
function refuseLocalOwnerSync() {
|
|
@@ -19,6 +20,34 @@ export async function syncPermissionRules(opts) {
|
|
|
19
20
|
if (typeof opts.principal !== "string" || opts.principal === "") {
|
|
20
21
|
throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
|
|
21
22
|
}
|
|
23
|
+
const traceClock = opts.now ?? Date.now;
|
|
24
|
+
const disclose = (result) => {
|
|
25
|
+
const tracer = opts.tracer;
|
|
26
|
+
if (tracer === undefined)
|
|
27
|
+
return result;
|
|
28
|
+
for (const r of result.resurrected) {
|
|
29
|
+
emitTrace(tracer, () => ({
|
|
30
|
+
kind: "permission.rule_sync_resurrected",
|
|
31
|
+
version: 1,
|
|
32
|
+
principal: opts.principal,
|
|
33
|
+
rule: r.rule,
|
|
34
|
+
scopeKind: r.scope.kind,
|
|
35
|
+
ts: traceClock(),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
for (const d of result.dropped) {
|
|
39
|
+
emitTrace(tracer, () => ({
|
|
40
|
+
kind: "permission.rule_sync_dropped",
|
|
41
|
+
version: 1,
|
|
42
|
+
principal: opts.principal,
|
|
43
|
+
rule: d.rule,
|
|
44
|
+
scopeKind: d.scope.kind,
|
|
45
|
+
reason: d.reason,
|
|
46
|
+
ts: traceClock(),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
22
51
|
const store = opts.provider.forPrincipal(opts.principal);
|
|
23
52
|
const writer = writerOf(store);
|
|
24
53
|
if (writer === undefined) {
|
|
@@ -111,7 +140,7 @@ export async function syncPermissionRules(opts) {
|
|
|
111
140
|
res = await writer.apply(delta, { expectedRev: current.rev });
|
|
112
141
|
}
|
|
113
142
|
catch (err) {
|
|
114
|
-
return {
|
|
143
|
+
return disclose({
|
|
115
144
|
ok: false,
|
|
116
145
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
117
146
|
landed: { newAdds: 0, newTombstones: 0 },
|
|
@@ -119,7 +148,7 @@ export async function syncPermissionRules(opts) {
|
|
|
119
148
|
dropped,
|
|
120
149
|
rev: current.rev,
|
|
121
150
|
warnings: [...warnings, `the store refused the sync landing: ${errText(err)} — nothing landed, the local state is unchanged`],
|
|
122
|
-
};
|
|
151
|
+
});
|
|
123
152
|
}
|
|
124
153
|
if ("conflict" in res) {
|
|
125
154
|
current = await writer.readRaw();
|
|
@@ -156,7 +185,7 @@ export async function syncPermissionRules(opts) {
|
|
|
156
185
|
if (!dropped.some((x) => x.dot.actor === q.dot.actor && x.dot.counter === q.dot.counter && x.reason === q.reason))
|
|
157
186
|
dropped.push(q);
|
|
158
187
|
}
|
|
159
|
-
return {
|
|
188
|
+
return disclose({
|
|
160
189
|
ok: dropped.length === 0 && warnings.length === 0,
|
|
161
190
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
162
191
|
landed: { newAdds, newTombstones },
|
|
@@ -164,9 +193,9 @@ export async function syncPermissionRules(opts) {
|
|
|
164
193
|
dropped,
|
|
165
194
|
rev: landedRaw.rev,
|
|
166
195
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
167
|
-
};
|
|
196
|
+
});
|
|
168
197
|
}
|
|
169
|
-
return {
|
|
198
|
+
return disclose({
|
|
170
199
|
ok: false,
|
|
171
200
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
172
201
|
landed: { newAdds: 0, newTombstones: 0 },
|
|
@@ -174,7 +203,7 @@ export async function syncPermissionRules(opts) {
|
|
|
174
203
|
dropped,
|
|
175
204
|
rev: current.rev,
|
|
176
205
|
warnings: [...warnings, `optimistic-concurrency retries exhausted after ${SYNC_MAX_ATTEMPTS} attempts — nothing landed, the local state is unchanged`],
|
|
177
|
-
};
|
|
206
|
+
});
|
|
178
207
|
}
|
|
179
208
|
function isDot(v) {
|
|
180
209
|
const d = v;
|
|
@@ -8,10 +8,10 @@ import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.
|
|
|
8
8
|
import { StoredSession } from "../session.js";
|
|
9
9
|
import type { SessionStore } from "../session.js";
|
|
10
10
|
import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
11
|
-
import type { OnAsk, ToolPolicy } from "../tool-policy.js";
|
|
11
|
+
import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
|
|
12
12
|
import { type ActiveSkillFrame } from "./active-skill-scope.js";
|
|
13
13
|
import type { SessionPermissionRules } from "../session-policy-store.js";
|
|
14
|
-
import { type Hooks } from "../hooks.js";
|
|
14
|
+
import { type Hooks, type OrgGateVerdict } from "../hooks.js";
|
|
15
15
|
import { type RecoveredOrphan } from "../session-reconcile.js";
|
|
16
16
|
import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
|
|
17
17
|
import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
|
|
@@ -214,6 +214,14 @@ export interface Prepared {
|
|
|
214
214
|
/** RB-63: the deployment's own caller policy, re-checked on a durable resume ONLY when the approver
|
|
215
215
|
* rewrote the pending call's args (see the composition site for why the edit case is special). */
|
|
216
216
|
basePolicyForResumeEdit?: ToolPolicy;
|
|
217
|
+
/** design/182 §7 — the ORG adjudication face, re-resolved on a durable RESUME before an approved
|
|
218
|
+
* pending call executes. The resume path bypasses the harness gate by design (a human already
|
|
219
|
+
* adjudicated the checkpointed call), which is exactly where org policy skew is most likely: the
|
|
220
|
+
* suspend may have outlived the snapshot revision that was current when it was minted. Present only
|
|
221
|
+
* on a governed deployment. */
|
|
222
|
+
permissionRuleOrg?: {
|
|
223
|
+
adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
|
|
224
|
+
};
|
|
217
225
|
/** Removes the `spec.signal` abort listener on task end (else a long-lived signal leaks listeners). */
|
|
218
226
|
releaseSignal: () => void;
|
|
219
227
|
/**
|
|
@@ -30,6 +30,7 @@ import { policyAskClassOf } from "../ask-class.js";
|
|
|
30
30
|
import { emitTrace } from "../trace.js";
|
|
31
31
|
import { createSessionRulePolicy } from "./session-rule-policy.js";
|
|
32
32
|
import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, runToolGate } from "../hooks.js";
|
|
33
|
+
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
33
34
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
34
35
|
import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
|
|
35
36
|
import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
|
|
@@ -3144,12 +3145,22 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3144
3145
|
};
|
|
3145
3146
|
const permissionRuleLane = (() => {
|
|
3146
3147
|
const provider = deps.permissionRuleStore;
|
|
3148
|
+
const localOwnerDeclared = deps.localOwnerRules === true;
|
|
3149
|
+
if (localOwnerDeclared) {
|
|
3150
|
+
if (provider === undefined) {
|
|
3151
|
+
throw new Error("RunnerDeps.localOwnerRules is declared but no permissionRuleStore provider is wired — there is no bucket for the local owner to hold rules in; refusing rather than running as if the declaration were absent");
|
|
3152
|
+
}
|
|
3153
|
+
if (provider.forLocalOwner === undefined) {
|
|
3154
|
+
throw new Error("RunnerDeps.localOwnerRules is declared but the wired permissionRuleStore provider implements no forLocalOwner() face — a provider without a local-owner bucket cannot honor the declaration; refusing rather than silently resolving zero rules");
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3147
3157
|
if (provider === undefined)
|
|
3148
3158
|
return undefined;
|
|
3149
3159
|
const root = taskRootPath;
|
|
3150
3160
|
return {
|
|
3151
3161
|
admits: async (req) => {
|
|
3152
|
-
|
|
3162
|
+
const anonymous = spec.principal === undefined || spec.principal === "";
|
|
3163
|
+
if (anonymous && !localOwnerDeclared)
|
|
3153
3164
|
return undefined;
|
|
3154
3165
|
if (req.toolName !== PERSISTED_RULE_TOOL)
|
|
3155
3166
|
return undefined;
|
|
@@ -3158,7 +3169,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3158
3169
|
return undefined;
|
|
3159
3170
|
let listed;
|
|
3160
3171
|
try {
|
|
3161
|
-
listed =
|
|
3172
|
+
listed = anonymous
|
|
3173
|
+
?
|
|
3174
|
+
await provider.forLocalOwner().list()
|
|
3175
|
+
: await provider.forPrincipal(spec.principal).list();
|
|
3162
3176
|
}
|
|
3163
3177
|
catch (err) {
|
|
3164
3178
|
emitTrace(deps.tracer, () => ({
|
|
@@ -3174,6 +3188,31 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3174
3188
|
},
|
|
3175
3189
|
};
|
|
3176
3190
|
})();
|
|
3191
|
+
const permissionRuleOrgLane = (() => {
|
|
3192
|
+
const overlay = deps.permissionRuleOrg;
|
|
3193
|
+
if (overlay === undefined)
|
|
3194
|
+
return undefined;
|
|
3195
|
+
return {
|
|
3196
|
+
adjudicate: async (req) => {
|
|
3197
|
+
if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
|
|
3198
|
+
return { status: "available" };
|
|
3199
|
+
let resolution;
|
|
3200
|
+
try {
|
|
3201
|
+
resolution = await overlay.resolve();
|
|
3202
|
+
}
|
|
3203
|
+
catch (err) {
|
|
3204
|
+
return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
|
|
3205
|
+
}
|
|
3206
|
+
if (resolution.status === "unavailable")
|
|
3207
|
+
return { status: "unavailable", disclosures: resolution.disclosures };
|
|
3208
|
+
const command = req.args?.command;
|
|
3209
|
+
if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
|
|
3210
|
+
return { status: "available" };
|
|
3211
|
+
const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
|
|
3212
|
+
return verdict === undefined ? { status: "available" } : { status: "available", verdict };
|
|
3213
|
+
},
|
|
3214
|
+
};
|
|
3215
|
+
})();
|
|
3177
3216
|
const ruleSuggestionsOf = (toolName, args) => {
|
|
3178
3217
|
if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
|
|
3179
3218
|
return {};
|
|
@@ -3621,6 +3660,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3621
3660
|
sessionDurability: resolveDeclaredDurability(sessions, "sessionStore"),
|
|
3622
3661
|
backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
|
|
3623
3662
|
permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
|
|
3663
|
+
permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
|
|
3664
|
+
permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
|
|
3624
3665
|
hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
|
|
3625
3666
|
lockedConfigWired: deps.lockedConfig !== undefined,
|
|
3626
3667
|
complianceWired: deps.compliancePostureResolver !== undefined,
|
|
@@ -3837,8 +3878,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3837
3878
|
return { ok: false };
|
|
3838
3879
|
if (memoryEngineSession)
|
|
3839
3880
|
await memoryEngineSession.harvest("checkpoint");
|
|
3840
|
-
|
|
3841
|
-
await checkpointStore.put(token, cp);
|
|
3881
|
+
const announceCommittedScreeningPark = () => {
|
|
3842
3882
|
const committedCount = cp.state.inheritedGate?.parentConstraintCount;
|
|
3843
3883
|
if (cp.state.inheritedGate?.requiresParentConstraint === true &&
|
|
3844
3884
|
!screeningParkDisclosedRef.done &&
|
|
@@ -3855,10 +3895,59 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3855
3895
|
catch {
|
|
3856
3896
|
}
|
|
3857
3897
|
}
|
|
3898
|
+
};
|
|
3899
|
+
const confirmPutOutcome = async (putErr) => {
|
|
3900
|
+
if (putErr?.code === "checkpoint.already_exists")
|
|
3901
|
+
return "absent";
|
|
3902
|
+
try {
|
|
3903
|
+
const row = await checkpointStore.get(token);
|
|
3904
|
+
if (row == null)
|
|
3905
|
+
return "absent";
|
|
3906
|
+
const sameCall = (a, b) => a.kind === b.kind && (a.kind !== "tool_approval" || b.kind !== "tool_approval" || a.toolCallId === b.toolCallId);
|
|
3907
|
+
const isOurs = row.scope === cp.scope && row.sessionId === cp.sessionId && row.leafId === cp.leafId && sameCall(row.pendingAction, cp.pendingAction);
|
|
3908
|
+
return isOurs ? "committed" : "absent";
|
|
3909
|
+
}
|
|
3910
|
+
catch (readErr) {
|
|
3911
|
+
deps.onError?.(readErr, { phase: "config", sessionId });
|
|
3912
|
+
return "unknown";
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
try {
|
|
3916
|
+
await checkpointStore.put(token, cp);
|
|
3917
|
+
announceCommittedScreeningPark();
|
|
3858
3918
|
return { ok: true };
|
|
3859
3919
|
}
|
|
3860
3920
|
catch (putErr) {
|
|
3861
3921
|
deps.onError?.(putErr, { phase: "config", sessionId });
|
|
3922
|
+
const confirmed = await confirmPutOutcome(putErr);
|
|
3923
|
+
if (confirmed === "committed") {
|
|
3924
|
+
try {
|
|
3925
|
+
deps.onError?.(new Error(`durable approval checkpoint: the store's write reported a failure but the row EXISTS — the commit ` +
|
|
3926
|
+
`landed and its acknowledgement was lost (a network backend can complete the INSERT and drop the ` +
|
|
3927
|
+
`connection before replying). The run suspends on that row rather than continuing, which would leave ` +
|
|
3928
|
+
`a redeemable pending checkpoint behind. This backend's acknowledgement path is lossy — the operator ` +
|
|
3929
|
+
`face carries the store's own message.`), { phase: "degraded", sessionId, classification: "checkpoint-put-confirmation-lost" });
|
|
3930
|
+
}
|
|
3931
|
+
catch {
|
|
3932
|
+
}
|
|
3933
|
+
announceCommittedScreeningPark();
|
|
3934
|
+
return { ok: true };
|
|
3935
|
+
}
|
|
3936
|
+
if (confirmed === "unknown") {
|
|
3937
|
+
const unknownReason = "the approval checkpoint's state cannot be established (the store rejected the write and then could not be read back; a committed-but-unacknowledged row may exist) — the run is stopped rather than continued past an approval whose durable record is unknown";
|
|
3938
|
+
try {
|
|
3939
|
+
deps.onError?.(new Error(`durable approval checkpoint: ${unknownReason}`), {
|
|
3940
|
+
phase: "degraded",
|
|
3941
|
+
sessionId,
|
|
3942
|
+
classification: "checkpoint-put-outcome-unknown",
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
catch {
|
|
3946
|
+
}
|
|
3947
|
+
abortController.abort();
|
|
3948
|
+
void harness.abort();
|
|
3949
|
+
return { ok: false, reason: unknownReason };
|
|
3950
|
+
}
|
|
3862
3951
|
const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
|
|
3863
3952
|
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
3864
3953
|
const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
|
|
@@ -4422,6 +4511,23 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4422
4511
|
},
|
|
4423
4512
|
}
|
|
4424
4513
|
: {}),
|
|
4514
|
+
...(permissionRuleOrgLane
|
|
4515
|
+
? {
|
|
4516
|
+
orgRules: {
|
|
4517
|
+
adjudicate: permissionRuleOrgLane.adjudicate,
|
|
4518
|
+
contentAskToolMounted: questionToolMounted,
|
|
4519
|
+
onUnavailable: (info) => emitTrace(deps.tracer, () => ({
|
|
4520
|
+
kind: "permission.org_snapshot_unavailable",
|
|
4521
|
+
version: 1,
|
|
4522
|
+
taskId: spec.taskId ?? sessionId,
|
|
4523
|
+
toolName: info.toolName,
|
|
4524
|
+
toolCallId: info.toolCallId,
|
|
4525
|
+
message: info.message,
|
|
4526
|
+
ts: Date.now(),
|
|
4527
|
+
})),
|
|
4528
|
+
},
|
|
4529
|
+
}
|
|
4530
|
+
: {}),
|
|
4425
4531
|
isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
|
|
4426
4532
|
...(sandboxAdmissionArmed
|
|
4427
4533
|
? {
|
|
@@ -4750,7 +4856,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4750
4856
|
: undefined;
|
|
4751
4857
|
overheadState.promptChars = systemPrompt.length;
|
|
4752
4858
|
const preparedHolder = {};
|
|
4753
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4859
|
+
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4754
4860
|
const prepared = buildPrepared();
|
|
4755
4861
|
preparedHolder.current = prepared;
|
|
4756
4862
|
return prepared;
|
|
@@ -9,6 +9,7 @@ import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQue
|
|
|
9
9
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
10
10
|
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
11
11
|
import { emitTrace } from "../trace.js";
|
|
12
|
+
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
|
|
12
13
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
13
14
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
14
15
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
@@ -4008,6 +4009,24 @@ export class Runner {
|
|
|
4008
4009
|
return;
|
|
4009
4010
|
}
|
|
4010
4011
|
}
|
|
4012
|
+
if (prepared.permissionRuleOrg !== undefined) {
|
|
4013
|
+
const orgVerdict = prepared.permissionRuleOrg
|
|
4014
|
+
.adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
|
|
4015
|
+
.catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
|
|
4016
|
+
const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
4017
|
+
const blocked = org.status === "unavailable"
|
|
4018
|
+
? "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
|
|
4019
|
+
: org.verdict?.behavior === "deny"
|
|
4020
|
+
? `an organization policy rule (${org.verdict.rule}) denies it`
|
|
4021
|
+
: undefined;
|
|
4022
|
+
if (blocked !== undefined) {
|
|
4023
|
+
const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}. This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
|
|
4024
|
+
emitEnd(true, { content: orgDenial });
|
|
4025
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
|
|
4026
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4011
4030
|
if (prepared.denyNarrowingPolicy) {
|
|
4012
4031
|
const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({
|
|
4013
4032
|
toolName: pendingAction.toolName,
|