@responsibleai/agent-hooks 0.1.0-alpha.2
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/README.md +47 -0
- package/agent-hooks.linux-x64-gnu.node +0 -0
- package/binding.d.ts +18 -0
- package/binding.js +327 -0
- package/dist/builder.d.ts +37 -0
- package/dist/builder.js +94 -0
- package/dist/ctk/index.d.ts +70 -0
- package/dist/ctk/index.js +18 -0
- package/dist/ctk/reference.d.ts +20 -0
- package/dist/ctk/reference.js +124 -0
- package/dist/ctk/runner.d.ts +26 -0
- package/dist/ctk/runner.js +144 -0
- package/dist/emitter.d.ts +111 -0
- package/dist/emitter.js +615 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +273 -0
- package/dist/native.d.ts +28 -0
- package/dist/native.js +56 -0
- package/package.json +36 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Microsoft Corporation.
|
|
3
|
+
// Licensed under the MIT License.
|
|
4
|
+
/**
|
|
5
|
+
* Reference in-memory agent + harness.
|
|
6
|
+
*
|
|
7
|
+
* The simplest possible conformant agent loop; exists so the
|
|
8
|
+
* CTK can self-test without a real framework. Port of
|
|
9
|
+
* `sdk/python/python/agent_hooks/ctk/reference.py`.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ReferenceHarness = void 0;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const index_1 = require("../index");
|
|
15
|
+
const builder_1 = require("../builder");
|
|
16
|
+
const emitter_1 = require("../emitter");
|
|
17
|
+
class ReferenceHarness {
|
|
18
|
+
name = "reference-agent";
|
|
19
|
+
capabilities = new Set(["model_calls", "tool_calls"]);
|
|
20
|
+
scenario;
|
|
21
|
+
emitter;
|
|
22
|
+
builder;
|
|
23
|
+
toolLog = [];
|
|
24
|
+
setup(scenario, interceptors, resolver, mode, composition, identityProvider) {
|
|
25
|
+
this.scenario = scenario;
|
|
26
|
+
this.toolLog = [];
|
|
27
|
+
const em = new emitter_1.InterceptionEmitter(mode, resolver);
|
|
28
|
+
em.setComposition(composition);
|
|
29
|
+
em.setIdentityProvider(identityProvider);
|
|
30
|
+
for (const i of interceptors)
|
|
31
|
+
em.register(i);
|
|
32
|
+
this.emitter = em;
|
|
33
|
+
this.builder = new builder_1.AgentContextBuilder({
|
|
34
|
+
agentId: "ref-agent",
|
|
35
|
+
framework: "reference-agent",
|
|
36
|
+
sessionId: (0, node_crypto_1.randomUUID)(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async run() {
|
|
40
|
+
const s = this.scenario;
|
|
41
|
+
const em = this.emitter;
|
|
42
|
+
const b = this.builder;
|
|
43
|
+
let outcome = "completed";
|
|
44
|
+
let final = null;
|
|
45
|
+
const tools = new Map((s.tools ?? []).map((t) => [t.name, t]));
|
|
46
|
+
const invokeTool = (name, args) => {
|
|
47
|
+
const spec = tools.get(name);
|
|
48
|
+
if (!spec)
|
|
49
|
+
throw new Error(`tool ${name} not in scenario`);
|
|
50
|
+
for (const bh of spec.behavior) {
|
|
51
|
+
if (!bh.when_args || JSON.stringify(bh.when_args) === JSON.stringify(args)) {
|
|
52
|
+
return { value: bh.return, is_error: bh.is_error ?? false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`tool ${name} invoked with ${JSON.stringify(args)}: no matching behavior`);
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
await em.emit(b.agentStartup([...tools.keys()].sort()));
|
|
59
|
+
await em.emit(b.input(s.input.content, s.input.role));
|
|
60
|
+
let messages = [
|
|
61
|
+
{ role: s.input.role, content: s.input.content },
|
|
62
|
+
];
|
|
63
|
+
for (const step of s.model_script ?? []) {
|
|
64
|
+
const resp = step.respond;
|
|
65
|
+
const preCtx = b.preModelCall("mock", [...messages]);
|
|
66
|
+
await em.emit(preCtx);
|
|
67
|
+
messages = preCtx.messages;
|
|
68
|
+
await em.emit(b.postModelCall("mock", resp.content, resp.tool_calls, resp.finish_reason));
|
|
69
|
+
if (resp.tool_calls.length > 0) {
|
|
70
|
+
for (const tc of resp.tool_calls) {
|
|
71
|
+
try {
|
|
72
|
+
const preTc = b.preToolCall(tc.id, tc.name, { ...tc.args });
|
|
73
|
+
await em.emit(preTc);
|
|
74
|
+
const args = preTc.tool_call.args;
|
|
75
|
+
const { value, is_error } = invokeTool(tc.name, args);
|
|
76
|
+
this.toolLog.push({ name: tc.name, args: { ...args } });
|
|
77
|
+
await em.emit(b.postToolCall(tc.id, tc.name, { ...args }, value, is_error));
|
|
78
|
+
messages.push({ role: "tool", content: value });
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof index_1.InterceptionBlocked) {
|
|
82
|
+
messages.push({ role: "tool", content: `blocked: ${e.result.verdict.reason}` });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
messages.push({ role: "assistant", content: resp.content ?? "" });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
final = resp.content;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (final !== null) {
|
|
97
|
+
const outCtx = b.output(final);
|
|
98
|
+
await em.emit(outCtx);
|
|
99
|
+
final = outCtx.output.content;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e instanceof index_1.InterceptionBlocked) {
|
|
104
|
+
outcome = "blocked";
|
|
105
|
+
final = null;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await em.emitUnchecked(b.agentShutdown(outcome === "completed" ? "completed" : "error"));
|
|
112
|
+
return {
|
|
113
|
+
outcome,
|
|
114
|
+
final_output: final,
|
|
115
|
+
tool_invocations: this.toolLog,
|
|
116
|
+
identities: em.records.map((r) => [r.input_identity, r.enforced_identity]),
|
|
117
|
+
records: em.records,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
teardown() {
|
|
121
|
+
// no-op
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.ReferenceHarness = ReferenceHarness;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CTK runner: load vectors, drive a harness, assert `expect`.
|
|
3
|
+
*
|
|
4
|
+
* The assertion engine, capability skip check, and scripted
|
|
5
|
+
* interceptor/resolver evaluation live in the Rust core (native.ctk*).
|
|
6
|
+
* This module keeps only vector globbing, the recording wrapper, and
|
|
7
|
+
* the orchestration loop that calls the native `Harness`.
|
|
8
|
+
*/
|
|
9
|
+
import { JsonValue } from "../index";
|
|
10
|
+
import type { Harness } from "./index";
|
|
11
|
+
/** TODO(stage-4): vectors still authored in the pre-P-003 five-verdict
|
|
12
|
+
* wire vocabulary (`warn`, `escalate`, `approval_resolver_missing`).
|
|
13
|
+
* Stage 4 rewrites them to the three-verdict shapes (§5.1); until then
|
|
14
|
+
* their scripted verdicts fail the §5 gate by design (fail closed) and
|
|
15
|
+
* no longer exercise the seam/warning semantics they were written for,
|
|
16
|
+
* so they are skipped here. */
|
|
17
|
+
export interface VectorResult {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
status: "pass" | "fail" | "skip";
|
|
21
|
+
detail: string;
|
|
22
|
+
failures: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function loadVectors(dir: string): JsonValue[];
|
|
25
|
+
export declare function runVector(harness: Harness, vector: JsonValue): Promise<VectorResult>;
|
|
26
|
+
export declare function runVectors(harnessFactory: () => Harness, vectors: JsonValue[]): Promise<VectorResult[]>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Microsoft Corporation.
|
|
3
|
+
// Licensed under the MIT License.
|
|
4
|
+
/**
|
|
5
|
+
* CTK runner: load vectors, drive a harness, assert `expect`.
|
|
6
|
+
*
|
|
7
|
+
* The assertion engine, capability skip check, and scripted
|
|
8
|
+
* interceptor/resolver evaluation live in the Rust core (native.ctk*).
|
|
9
|
+
* This module keeps only vector globbing, the recording wrapper, and
|
|
10
|
+
* the orchestration loop that calls the native `Harness`.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.loadVectors = loadVectors;
|
|
14
|
+
exports.runVector = runVector;
|
|
15
|
+
exports.runVectors = runVectors;
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
const index_1 = require("../index");
|
|
19
|
+
const native_1 = require("../native");
|
|
20
|
+
function loadVectors(dir) {
|
|
21
|
+
const vectors = (0, node_fs_1.readdirSync)(dir)
|
|
22
|
+
.filter((f) => /^AH-CTK-.*\.json$/.test(f))
|
|
23
|
+
.sort()
|
|
24
|
+
.map((f) => JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, f), "utf8")));
|
|
25
|
+
if (vectors.length === 0) {
|
|
26
|
+
// A runner fed zero vectors reports 100% pass — a false
|
|
27
|
+
// conformance signal (§13.2). Fail loudly instead.
|
|
28
|
+
throw new Error(`no AH-CTK-*.json vectors found in ${dir}`);
|
|
29
|
+
}
|
|
30
|
+
return vectors;
|
|
31
|
+
}
|
|
32
|
+
/** Replays one `interceptor_script` rule list via the Rust core. */
|
|
33
|
+
class ScriptedInterceptor {
|
|
34
|
+
rulesJson;
|
|
35
|
+
constructor(rules) {
|
|
36
|
+
this.rulesJson = JSON.stringify(rules);
|
|
37
|
+
}
|
|
38
|
+
intercept(ctx) {
|
|
39
|
+
const w = JSON.parse(native_1.native.ctkScriptedIntercept(this.rulesJson, JSON.stringify(ctx)));
|
|
40
|
+
if (w !== null && typeof w === "object" && "__ctk_fault__" in w) {
|
|
41
|
+
// NOW-10 fault injection: exercise §6.3 interceptor_failed.
|
|
42
|
+
throw new Error("ctk scripted fault: raise");
|
|
43
|
+
}
|
|
44
|
+
return w;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Wraps the scripted interceptor and records every ctx passed. */
|
|
48
|
+
class RecordingInterceptor extends ScriptedInterceptor {
|
|
49
|
+
recorded = [];
|
|
50
|
+
intercept(ctx) {
|
|
51
|
+
this.recorded.push(JSON.parse(JSON.stringify(ctx)));
|
|
52
|
+
return super.intercept(ctx);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class ScriptedResolver {
|
|
56
|
+
rulesJson;
|
|
57
|
+
constructor(rules) {
|
|
58
|
+
this.rulesJson = JSON.stringify(rules);
|
|
59
|
+
}
|
|
60
|
+
resolve(req) {
|
|
61
|
+
// §10.1: identity may be null (null provider). The scripted engine
|
|
62
|
+
// works in strings; "" round-trips to null below.
|
|
63
|
+
const requestIdentity = req.context_identity ?? "";
|
|
64
|
+
const r = JSON.parse(native_1.native.ctkScriptedResolve(this.rulesJson, JSON.stringify(req.context), requestIdentity));
|
|
65
|
+
if (r !== null && typeof r === "object" && "__ctk_fault__" in r) {
|
|
66
|
+
// NOW-10 fault injection: exercise §9 approval_resolver_failed.
|
|
67
|
+
throw new Error("ctk scripted fault: raise");
|
|
68
|
+
}
|
|
69
|
+
if (r.context_identity === "" && req.context_identity === null) {
|
|
70
|
+
r.context_identity = null;
|
|
71
|
+
}
|
|
72
|
+
return r;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function runRecordToWire(rr) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
outcome: rr.outcome,
|
|
78
|
+
final_output: rr.final_output ?? null,
|
|
79
|
+
tool_invocations: rr.tool_invocations,
|
|
80
|
+
error: rr.error ?? null,
|
|
81
|
+
identities: rr.identities.map(([i, e]) => ({ input_identity: i, enforced_identity: e })),
|
|
82
|
+
records: rr.records,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function runVector(harness, vector) {
|
|
86
|
+
const v = vector;
|
|
87
|
+
const vectorJson = JSON.stringify(vector);
|
|
88
|
+
const capsJson = JSON.stringify([...harness.capabilities].sort());
|
|
89
|
+
const skip = JSON.parse(native_1.native.ctkShouldSkip(vectorJson, capsJson));
|
|
90
|
+
if (skip !== null) {
|
|
91
|
+
return {
|
|
92
|
+
id: v.id,
|
|
93
|
+
title: v.title,
|
|
94
|
+
status: "skip",
|
|
95
|
+
detail: skip,
|
|
96
|
+
failures: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Multi-interceptor vectors (§7.1 fold-through) use interceptor_scripts;
|
|
100
|
+
// single-interceptor vectors use interceptor_script. Only the FIRST
|
|
101
|
+
// interceptor records: expect.interceptions describes each emission as
|
|
102
|
+
// the first-registered interceptor saw it. An empty interceptor_scripts
|
|
103
|
+
// registers zero interceptors (§7 fail-closed vector).
|
|
104
|
+
const scripts = v.interceptor_scripts ?? [v.interceptor_script];
|
|
105
|
+
const first = scripts.length > 0 ? new RecordingInterceptor(scripts[0]) : null;
|
|
106
|
+
const interceptors = first ? [first] : [];
|
|
107
|
+
for (const s of scripts.slice(1))
|
|
108
|
+
interceptors.push(new ScriptedInterceptor(s));
|
|
109
|
+
const approval = v.approval_script;
|
|
110
|
+
// NB: [] is truthy in JS — an empty approval_script registers NO
|
|
111
|
+
// resolver (matches the Rust/Python runners; exercised by AH-CTK-032).
|
|
112
|
+
const resolver = approval && approval.length > 0 ? new ScriptedResolver(approval) : null;
|
|
113
|
+
const mode = (v.mode ?? "enforce");
|
|
114
|
+
// §13.2: composition vectors carry the profile/knobs they apply to;
|
|
115
|
+
// absent means the pre-P-003 default (`sequential/first_deny, stop`).
|
|
116
|
+
const composition = v.composition ?? index_1.Composition.default();
|
|
117
|
+
// §10.1: absent → the default provider; explicit null → unbound.
|
|
118
|
+
const identityProvider = 'identity_provider' in v ? v.identity_provider : 'jcs-sha256';
|
|
119
|
+
harness.setup(v.scenario, interceptors, resolver, mode, composition, identityProvider);
|
|
120
|
+
let rr;
|
|
121
|
+
try {
|
|
122
|
+
rr = await harness.run();
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
return {
|
|
126
|
+
id: v.id,
|
|
127
|
+
title: v.title,
|
|
128
|
+
status: "fail",
|
|
129
|
+
detail: "",
|
|
130
|
+
failures: [`harness.run threw: ${e}`],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
harness.teardown();
|
|
135
|
+
}
|
|
136
|
+
return JSON.parse(native_1.native.ctkAssert(vectorJson, JSON.stringify(first?.recorded ?? []), runRecordToWire(rr)));
|
|
137
|
+
}
|
|
138
|
+
async function runVectors(harnessFactory, vectors) {
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const v of vectors) {
|
|
141
|
+
out.push(await runVector(harnessFactory(), v));
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side emitter: dispatch context → interceptors → composition →
|
|
3
|
+
* combined verdict → record (§6–§10).
|
|
4
|
+
*
|
|
5
|
+
* Per-language orchestrator over the Rust core:
|
|
6
|
+
*
|
|
7
|
+
* - Interceptor dispatch (§7) and approval-seam resolution (§9) stay here
|
|
8
|
+
* because they call back into user JS code.
|
|
9
|
+
* - Verdict validation (§5), transform application (§5.2, §7.4),
|
|
10
|
+
* severity-max aggregation (§7.3, via `composeAggregate`), identity
|
|
11
|
+
* computation (§10), and record building (§10.3) delegate to the Rust
|
|
12
|
+
* core so behaviour is byte-identical across SDKs.
|
|
13
|
+
*
|
|
14
|
+
* Composition is host configuration (§7.1): the profile is set on the
|
|
15
|
+
* emitter (default `sequential/first_deny, on_approval: stop`) and
|
|
16
|
+
* recorded on every emission. "Parallel" profiles are implemented with
|
|
17
|
+
* serial dispatch over isolated snapshots — §7.2: parallel names
|
|
18
|
+
* isolation semantics, not scheduling.
|
|
19
|
+
*
|
|
20
|
+
* Fail-closed defaults: an `enforce`-mode emission with zero registered
|
|
21
|
+
* interceptors yields `deny host_error:no_interceptor` (§7), and
|
|
22
|
+
* {@link InterceptionEmitter.emit} **throws** {@link InterceptionBlocked}
|
|
23
|
+
* on any block — the ignorable-result variant is the explicitly named
|
|
24
|
+
* {@link InterceptionEmitter.emitUnchecked}.
|
|
25
|
+
*
|
|
26
|
+
* Concurrency (§12.2): emissions for different tool calls may interleave
|
|
27
|
+
* on the event loop; sequence assignment and record append are atomic on
|
|
28
|
+
* a single JS thread. Sharing one emitter across workers is unsupported.
|
|
29
|
+
*/
|
|
30
|
+
import { AgentContext, ApprovalResolver, CompositionConfig, EnforcementMode, IdentityProvider, InterceptionRecord, Interceptor } from "./index";
|
|
31
|
+
/** §7 RECOMMENDED interceptor/resolver timeout (milliseconds). */
|
|
32
|
+
export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
33
|
+
export declare class InterceptionEmitter {
|
|
34
|
+
private readonly mode;
|
|
35
|
+
private readonly resolver;
|
|
36
|
+
private readonly timeoutMs;
|
|
37
|
+
private readonly interceptors;
|
|
38
|
+
private readonly _records;
|
|
39
|
+
private composition;
|
|
40
|
+
private identity;
|
|
41
|
+
/**
|
|
42
|
+
* `timeoutMs` bounds each interceptor `intercept()` and resolver
|
|
43
|
+
* `resolve()` call (§7, RECOMMENDED default 5000 ms); breach fails
|
|
44
|
+
* closed with `host_error:interceptor_timeout` (interceptor) or
|
|
45
|
+
* `host_error:approval_resolver_failed` (resolver). Only async work
|
|
46
|
+
* can be preempted — a synchronous interceptor that blocks the event
|
|
47
|
+
* loop cannot be interrupted. `timeoutMs: null` disables enforcement.
|
|
48
|
+
*/
|
|
49
|
+
constructor(mode?: EnforcementMode, resolver?: ApprovalResolver | null, timeoutMs?: number | null);
|
|
50
|
+
/** Race `fn`'s result against the §7 timeout. */
|
|
51
|
+
private withTimeout;
|
|
52
|
+
get records(): readonly InterceptionRecord[];
|
|
53
|
+
register(interceptor: Interceptor): this;
|
|
54
|
+
/** Declare the composition profile for subsequent emissions (§7.1). */
|
|
55
|
+
setComposition(composition: CompositionConfig): this;
|
|
56
|
+
/** Declare the identity provider (§10.1). Default `"jcs-sha256"`. */
|
|
57
|
+
setIdentityProvider(provider: IdentityProvider): this;
|
|
58
|
+
/** Run the emission and **throw** {@link InterceptionBlocked} if the
|
|
59
|
+
* guarded action must not proceed (§6). Primary entry point. */
|
|
60
|
+
emit(ctx: AgentContext): Promise<InterceptionRecord>;
|
|
61
|
+
/** Run the emission and return the record without throwing. The
|
|
62
|
+
* caller MUST inspect `proceeds` and halt the guarded action itself;
|
|
63
|
+
* prefer {@link emit}. */
|
|
64
|
+
emitUnchecked(ctx: AgentContext): Promise<InterceptionRecord>;
|
|
65
|
+
/** Profile dispatch (§7.4–§7.5). Returns the combined verdict and
|
|
66
|
+
* its record metadata. */
|
|
67
|
+
private dispatch;
|
|
68
|
+
/** Invoke one interceptor on its own copy of `ctx` (§7) and pass the
|
|
69
|
+
* result through the §5 gate. Never throws: every failure becomes the
|
|
70
|
+
* §6.3 synthesized deny. */
|
|
71
|
+
private invoke;
|
|
72
|
+
/** §5 gate: normalize a wire verdict or synthesize the §6.3 deny. The
|
|
73
|
+
* non-finite pre-scan runs first — JSON.stringify would silently
|
|
74
|
+
* corrupt NaN/±Infinity to null before the core could reject them. */
|
|
75
|
+
private gate;
|
|
76
|
+
/** `sequential/first_deny` (§7.4): fold-through, first deny
|
|
77
|
+
* short-circuits; a liftable deny consults the seam, then `stop` or
|
|
78
|
+
* `resume` per the knob.
|
|
79
|
+
*
|
|
80
|
+
* `perInterceptor` stays index-aligned with registration order (one
|
|
81
|
+
* entry per invoked interceptor, §10.3 summaries); `pool`
|
|
82
|
+
* additionally holds substituted resolutions for the §7.3 unions. */
|
|
83
|
+
private dispatchFirstDeny;
|
|
84
|
+
/** `sequential/run_all` (§7.4): everything runs, transforms fold
|
|
85
|
+
* through for visibility, severity-max aggregate; the seam is
|
|
86
|
+
* consulted at most once, only when the winner is liftable (which
|
|
87
|
+
* implies every deny in the emission is liftable — a single plain
|
|
88
|
+
* deny already won the severity order). */
|
|
89
|
+
private dispatchRunAll;
|
|
90
|
+
/** Parallel profiles (§7.5): isolated snapshots, no fold; serial
|
|
91
|
+
* dispatch (isolation semantics, not scheduling). */
|
|
92
|
+
private dispatchParallel;
|
|
93
|
+
/** Severity-max aggregation + winner handling, shared by `run_all`
|
|
94
|
+
* and both parallel profiles. Aggregation — including the §7.5
|
|
95
|
+
* transform-conflict and `parallel/unanimous` disagreement synthesis
|
|
96
|
+
* per the declared knobs — delegates to the core's
|
|
97
|
+
* `composeAggregate` so all SDKs agree; the seam consultation and
|
|
98
|
+
* transform application stay here (they call back into user JS). */
|
|
99
|
+
private aggregateAndConsult;
|
|
100
|
+
/** Apply (enforce) or validate (evaluate_only) one transform (§7.4, §8). */
|
|
101
|
+
private foldTransform;
|
|
102
|
+
/** The declared provider's output for `ctx` (§10.1); `null` iff the
|
|
103
|
+
* provider is `null`. Throws {@link AgentHooksCoreError} iff the
|
|
104
|
+
* provider rejected the context (§10.2 fail-closed value domain). */
|
|
105
|
+
private identityOf;
|
|
106
|
+
/** Consult the approval seam for a liftable deny (§9), when the
|
|
107
|
+
* profile conditions allow it: `enforce` mode, not `agent_shutdown`,
|
|
108
|
+
* a resolver registered, and the verdict actually liftable. Enforces
|
|
109
|
+
* the echo rule and the §9 outcome/verdict consistency requirements. */
|
|
110
|
+
private consult;
|
|
111
|
+
}
|