agentic-workflow-manager 9.11.11 → 9.12.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/dist/src/commands/doctor.js +23 -1
- package/dist/src/commands/hooks/claude.js +61 -8
- package/dist/src/commands/hooks/resync.js +1 -1
- package/dist/src/commands/job/index.js +39 -4
- package/dist/src/commands/model-policy/index.js +253 -0
- package/dist/src/commands/plan/index.js +53 -12
- package/dist/src/commands/preflight/checks.js +18 -0
- package/dist/src/commands/watch/apply.js +71 -5
- package/dist/src/commands/watch/supervisor.js +63 -3
- package/dist/src/core/admission/index.js +20 -6
- package/dist/src/core/journal/types.js +21 -5
- package/dist/src/core/model-policy/capabilities-v2.js +152 -0
- package/dist/src/core/model-policy/capabilities.js +7 -1
- package/dist/src/core/model-policy/claude-hook-events.js +120 -0
- package/dist/src/core/model-policy/event-store.js +221 -0
- package/dist/src/core/model-policy/hook-diagnostics.js +94 -0
- package/dist/src/core/model-policy/journal.js +125 -16
- package/dist/src/core/model-policy/local-claude-scope.js +41 -0
- package/dist/src/core/model-policy/local-codex-scope.js +33 -0
- package/dist/src/core/model-policy/local-event-scope.js +13 -0
- package/dist/src/core/model-policy/machine-key.js +56 -0
- package/dist/src/core/model-policy/native-claude-agent-definition.js +82 -0
- package/dist/src/core/model-policy/native-claude-config.js +107 -0
- package/dist/src/core/model-policy/native-claude-machine.js +58 -0
- package/dist/src/core/model-policy/native-claude-transcript.js +80 -0
- package/dist/src/core/model-policy/native-claude.js +41 -0
- package/dist/src/core/model-policy/native-codex-machine.js +116 -0
- package/dist/src/core/model-policy/native-codex-rollout.js +111 -0
- package/dist/src/core/model-policy/native-codex.js +302 -0
- package/dist/src/core/model-policy/native-routing-observe.js +106 -0
- package/dist/src/core/model-policy/native-routing-proof.js +52 -0
- package/dist/src/core/model-policy/native-runtime.js +86 -0
- package/dist/src/core/model-policy/resolve.js +117 -3
- package/dist/src/core/model-policy/selection-policy-digest.js +22 -0
- package/dist/src/core/model-policy/setup-readiness.js +77 -0
- package/dist/src/providers/index.js +3 -1
- package/dist/tests/commands/doctor-routing.test.js +22 -0
- package/dist/tests/commands/doctor.test.js +5 -0
- package/dist/tests/commands/hooks/install.test.js +6 -0
- package/dist/tests/commands/hooks/resync.test.js +3 -0
- package/dist/tests/commands/hooks/status.test.js +12 -1
- package/dist/tests/commands/job/routing-file.test.js +32 -0
- package/dist/tests/commands/job/routing-report-cli.test.js +12 -5
- package/dist/tests/commands/job/routing.test.js +2 -2
- package/dist/tests/commands/model-policy/claude-hook-flow.test.js +104 -0
- package/dist/tests/commands/model-policy/index.test.js +179 -0
- package/dist/tests/commands/plan/index.test.js +2 -2
- package/dist/tests/commands/preflight/preflight.test.js +65 -0
- package/dist/tests/commands/watch/apply.test.js +116 -10
- package/dist/tests/commands/watch/dispatch-routing-identity.test.js +23 -0
- package/dist/tests/core/admission/index.test.js +21 -0
- package/dist/tests/core/model-policy/capabilities-v2.test.js +42 -0
- package/dist/tests/core/model-policy/claude-hook-events.test.js +40 -0
- package/dist/tests/core/model-policy/event-store-claude.test.js +64 -0
- package/dist/tests/core/model-policy/event-store.test.js +151 -0
- package/dist/tests/core/model-policy/hook-diagnostics.test.js +34 -0
- package/dist/tests/core/model-policy/journal.test.js +165 -12
- package/dist/tests/core/model-policy/local-claude-scope.test.js +31 -0
- package/dist/tests/core/model-policy/machine-key.test.js +38 -0
- package/dist/tests/core/model-policy/native-claude-agent-definition.test.js +31 -0
- package/dist/tests/core/model-policy/native-claude-config.test.js +53 -0
- package/dist/tests/core/model-policy/native-claude-machine.test.js +18 -0
- package/dist/tests/core/model-policy/native-claude-transcript.test.js +45 -0
- package/dist/tests/core/model-policy/native-claude.test.js +21 -0
- package/dist/tests/core/model-policy/native-codex-machine.test.js +34 -0
- package/dist/tests/core/model-policy/native-codex-rollout.test.js +44 -0
- package/dist/tests/core/model-policy/native-codex.test.js +133 -0
- package/dist/tests/core/model-policy/native-routing-observe.test.js +60 -0
- package/dist/tests/core/model-policy/native-routing-proof.test.js +30 -0
- package/dist/tests/core/model-policy/native-runtime.test.js +14 -0
- package/dist/tests/core/model-policy/resolve-event.test.js +65 -0
- package/dist/tests/core/model-policy/resolve.test.js +17 -0
- package/dist/tests/core/model-policy/setup-readiness.test.js +83 -0
- package/dist/tests/helpers/routing-approval.js +14 -0
- package/dist/tests/providers/index.test.js +1 -1
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateEventReceipt = validateEventReceipt;
|
|
4
|
+
exports.evaluateEventReceipt = evaluateEventReceipt;
|
|
5
|
+
const capabilities_1 = require("./capabilities");
|
|
6
|
+
const validate_1 = require("./validate");
|
|
7
|
+
function record(value, label) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
|
+
throw new Error(`${label} must be an object`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function exact(value, keys, label) {
|
|
13
|
+
if (Object.keys(value).length !== keys.length || Object.keys(value).some(key => !keys.includes(key)))
|
|
14
|
+
throw new Error(`${label} has unknown or missing fields`);
|
|
15
|
+
}
|
|
16
|
+
function digest(value, label) {
|
|
17
|
+
(0, validate_1.assertDigest)(value, label);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function selection(value) {
|
|
21
|
+
const item = record(value, 'selection');
|
|
22
|
+
exact(item, ['selector', 'effort'], 'selection');
|
|
23
|
+
const selector = record(item.selector, 'selection.selector');
|
|
24
|
+
exact(selector, ['kind', 'id'], 'selection.selector');
|
|
25
|
+
if (selector.kind !== 'model' && selector.kind !== 'tier')
|
|
26
|
+
throw new Error('selection.selector.kind is invalid');
|
|
27
|
+
if (typeof selector.id !== 'string' || selector.id.length === 0 || selector.id.length > 128 || /[\u0000-\u001f\u007f-\u009f]/.test(selector.id))
|
|
28
|
+
throw new Error('selection.selector.id is invalid');
|
|
29
|
+
const effort = record(item.effort, 'selection.effort');
|
|
30
|
+
if (effort.kind === 'runtime-default') {
|
|
31
|
+
exact(effort, ['kind'], 'selection.effort');
|
|
32
|
+
return { selector: { kind: selector.kind, id: selector.id }, effort: { kind: 'runtime-default' } };
|
|
33
|
+
}
|
|
34
|
+
exact(effort, ['kind', 'value'], 'selection.effort');
|
|
35
|
+
if (effort.kind !== 'explicit' || typeof effort.value !== 'string' || effort.value.length === 0 || effort.value.length > 128 || /[\u0000-\u001f\u007f-\u009f]/.test(effort.value))
|
|
36
|
+
throw new Error('selection.effort is invalid');
|
|
37
|
+
return { selector: { kind: selector.kind, id: selector.id }, effort: { kind: 'explicit', value: effort.value } };
|
|
38
|
+
}
|
|
39
|
+
function key(value) { return `${value.selector.kind}\0${value.selector.id}\0${value.effort.kind}\0${value.effort.kind === 'explicit' ? value.effort.value : ''}`; }
|
|
40
|
+
function finiteTokens(value, label) {
|
|
41
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
42
|
+
throw new Error(`${label} is invalid`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/** Schema v2 is internal native evidence, never a candidate for v1 approval. */
|
|
46
|
+
function validateEventReceipt(value) {
|
|
47
|
+
const item = record(value, 'event receipt');
|
|
48
|
+
exact(item, ['schema', 'runtime', 'binaryDigest', 'configDigest', 'recordedAt', 'claims'], 'event receipt');
|
|
49
|
+
if (item.schema !== 'routing-capabilities/v2')
|
|
50
|
+
throw new Error('event receipt schema is invalid');
|
|
51
|
+
const runtime = (0, capabilities_1.validateRuntimeKey)(item.runtime);
|
|
52
|
+
if (runtime.target !== 'codex' && runtime.target !== 'claude-code')
|
|
53
|
+
throw new Error('event receipt provider target is unsupported');
|
|
54
|
+
const binaryDigest = digest(item.binaryDigest, 'binaryDigest');
|
|
55
|
+
const configDigest = digest(item.configDigest, 'configDigest');
|
|
56
|
+
const recordedAt = (0, validate_1.canonicalUtcTimestamp)(item.recordedAt, 'recordedAt');
|
|
57
|
+
if (!Array.isArray(item.claims) || item.claims.length === 0 || item.claims.length > 64)
|
|
58
|
+
throw new Error('event receipt claims are invalid');
|
|
59
|
+
const claims = item.claims.map((raw, index) => {
|
|
60
|
+
const claim = record(raw, `claims[${index}]`);
|
|
61
|
+
exact(claim, claim.source === 'claude-assistant-transcript'
|
|
62
|
+
? ['selection', 'mappingDigest', 'eventDigest', 'source', 'nativeAgentType', ...(claim.nativeAgentIdDigest === undefined ? [] : ['nativeAgentIdDigest']), ...(claim.nativeEvents === undefined ? [] : ['nativeEvents']), 'observedAt', 'actualModel', 'tokenUsage']
|
|
63
|
+
: ['selection', 'mappingDigest', 'eventDigest', 'source', 'observedAt', 'actualModel', 'tokenUsage'], `claims[${index}]`);
|
|
64
|
+
const selected = selection(claim.selection);
|
|
65
|
+
const mappingDigest = digest(claim.mappingDigest, `claims[${index}].mappingDigest`);
|
|
66
|
+
const eventDigest = digest(claim.eventDigest, `claims[${index}].eventDigest`);
|
|
67
|
+
if (claim.source !== 'codex-turn-context' && claim.source !== 'claude-assistant-transcript')
|
|
68
|
+
throw new Error(`claims[${index}].source is invalid`);
|
|
69
|
+
if ((runtime.target === 'codex') !== (claim.source === 'codex-turn-context'))
|
|
70
|
+
throw new Error(`claims[${index}].source does not match provider`);
|
|
71
|
+
if (claim.source === 'claude-assistant-transcript' && (typeof claim.nativeAgentType !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(claim.nativeAgentType)))
|
|
72
|
+
throw new Error(`claims[${index}].nativeAgentType is invalid`);
|
|
73
|
+
if (claim.nativeAgentIdDigest !== undefined) {
|
|
74
|
+
if (claim.source !== 'claude-assistant-transcript')
|
|
75
|
+
throw new Error(`claims[${index}].nativeAgentIdDigest is invalid`);
|
|
76
|
+
digest(claim.nativeAgentIdDigest, `claims[${index}].nativeAgentIdDigest`);
|
|
77
|
+
}
|
|
78
|
+
let nativeEvents;
|
|
79
|
+
if (claim.nativeEvents !== undefined) {
|
|
80
|
+
if (claim.source !== 'claude-assistant-transcript' || !Array.isArray(claim.nativeEvents)
|
|
81
|
+
|| claim.nativeEvents.length === 0 || claim.nativeEvents.length > 64)
|
|
82
|
+
throw new Error(`claims[${index}].nativeEvents is invalid`);
|
|
83
|
+
nativeEvents = claim.nativeEvents.map((raw, eventIndex) => {
|
|
84
|
+
const event = record(raw, `claims[${index}].nativeEvents[${eventIndex}]`);
|
|
85
|
+
exact(event, ['nativeAgentIdDigest', 'eventDigest', 'observedAt'], `claims[${index}].nativeEvents[${eventIndex}]`);
|
|
86
|
+
const observedAt = (0, validate_1.canonicalUtcTimestamp)(event.observedAt, `claims[${index}].nativeEvents[${eventIndex}].observedAt`);
|
|
87
|
+
if (Date.parse(observedAt) > Date.parse(recordedAt))
|
|
88
|
+
throw new Error('Claude native event is newer than receipt');
|
|
89
|
+
return { nativeAgentIdDigest: digest(event.nativeAgentIdDigest, 'nativeAgentIdDigest'),
|
|
90
|
+
eventDigest: digest(event.eventDigest, 'eventDigest'), observedAt };
|
|
91
|
+
});
|
|
92
|
+
if (new Set(nativeEvents.map(event => event.nativeAgentIdDigest)).size !== nativeEvents.length
|
|
93
|
+
|| new Set(nativeEvents.map(event => event.eventDigest)).size !== nativeEvents.length)
|
|
94
|
+
throw new Error('Claude native events are duplicated');
|
|
95
|
+
}
|
|
96
|
+
const observedAt = (0, validate_1.canonicalUtcTimestamp)(claim.observedAt, `claims[${index}].observedAt`);
|
|
97
|
+
if (Date.parse(observedAt) > Date.parse(recordedAt))
|
|
98
|
+
throw new Error(`claims[${index}] is newer than receipt`);
|
|
99
|
+
let actualModel;
|
|
100
|
+
if (claim.actualModel === 'unverified')
|
|
101
|
+
actualModel = 'unverified';
|
|
102
|
+
else {
|
|
103
|
+
const actual = record(claim.actualModel, `claims[${index}].actualModel`);
|
|
104
|
+
exact(actual, ['id', 'evidenceDigest'], `claims[${index}].actualModel`);
|
|
105
|
+
if (claim.source !== 'claude-assistant-transcript' || typeof actual.id !== 'string' || actual.id.length === 0 || actual.id.length > 128)
|
|
106
|
+
throw new Error(`claims[${index}].actualModel is invalid`);
|
|
107
|
+
actualModel = { id: actual.id, evidenceDigest: digest(actual.evidenceDigest, `claims[${index}].actualModel.evidenceDigest`) };
|
|
108
|
+
}
|
|
109
|
+
let tokenUsage;
|
|
110
|
+
if (claim.tokenUsage === 'unknown')
|
|
111
|
+
tokenUsage = 'unknown';
|
|
112
|
+
else {
|
|
113
|
+
const usage = record(claim.tokenUsage, `claims[${index}].tokenUsage`);
|
|
114
|
+
exact(usage, ['input', 'output'], `claims[${index}].tokenUsage`);
|
|
115
|
+
tokenUsage = { input: finiteTokens(usage.input, 'input tokens'), output: finiteTokens(usage.output, 'output tokens') };
|
|
116
|
+
}
|
|
117
|
+
return { selection: selected, mappingDigest, eventDigest, source: claim.source,
|
|
118
|
+
...(claim.source === 'claude-assistant-transcript' ? { nativeAgentType: claim.nativeAgentType,
|
|
119
|
+
...(claim.nativeAgentIdDigest === undefined ? {} : { nativeAgentIdDigest: claim.nativeAgentIdDigest }),
|
|
120
|
+
...(nativeEvents === undefined ? {} : { nativeEvents }) } : {}), observedAt, actualModel, tokenUsage };
|
|
121
|
+
});
|
|
122
|
+
if (new Set(claims.map(claim => key(claim.selection))).size !== claims.length)
|
|
123
|
+
throw new Error('event receipt has duplicate selections');
|
|
124
|
+
return { schema: 'routing-capabilities/v2', runtime, binaryDigest, configDigest, recordedAt, claims };
|
|
125
|
+
}
|
|
126
|
+
/** Local, read-only comparison: no TTL, network call, rewrite or paid probe. */
|
|
127
|
+
function evaluateEventReceipt(value, scopeValue, selectedValue, mappingDigestValue, now) {
|
|
128
|
+
const receipt = validateEventReceipt(value);
|
|
129
|
+
const scope = record(scopeValue, 'event scope');
|
|
130
|
+
exact(scope, ['runtime', 'binaryDigest', 'configDigest'], 'event scope');
|
|
131
|
+
const runtime = (0, capabilities_1.validateRuntimeKey)(scope.runtime);
|
|
132
|
+
const binaryDigest = digest(scope.binaryDigest, 'binaryDigest');
|
|
133
|
+
const configDigest = digest(scope.configDigest, 'configDigest');
|
|
134
|
+
const selected = selection(selectedValue);
|
|
135
|
+
const mappingDigest = digest(mappingDigestValue, 'mappingDigest');
|
|
136
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
137
|
+
throw new Error('event currentness clock is invalid');
|
|
138
|
+
if (Date.parse(receipt.recordedAt) > now.getTime())
|
|
139
|
+
return { state: 'drift', reason: 'FUTURE_EVIDENCE' };
|
|
140
|
+
if (receipt.runtime.target !== runtime.target || receipt.runtime.kind !== runtime.kind || receipt.runtime.version !== runtime.version || receipt.binaryDigest !== binaryDigest)
|
|
141
|
+
return { state: 'drift', reason: 'RUNTIME_DRIFT' };
|
|
142
|
+
if (receipt.runtime.accountScopeDigest !== runtime.accountScopeDigest)
|
|
143
|
+
return { state: 'drift', reason: 'ACCOUNT_DRIFT' };
|
|
144
|
+
if (receipt.configDigest !== configDigest)
|
|
145
|
+
return { state: 'drift', reason: 'CONFIG_DRIFT' };
|
|
146
|
+
const claim = receipt.claims.find(item => key(item.selection) === key(selected));
|
|
147
|
+
if (!claim)
|
|
148
|
+
return { state: 'drift', reason: 'SELECTION_UNENROLLED' };
|
|
149
|
+
if (claim.mappingDigest !== mappingDigest)
|
|
150
|
+
return { state: 'drift', reason: 'POLICY_DRIFT' };
|
|
151
|
+
return { state: 'current' };
|
|
152
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.capabilityReceiptDigest = capabilityReceiptDigest;
|
|
|
8
8
|
exports.validateCapabilityReceipt = validateCapabilityReceipt;
|
|
9
9
|
exports.receiptIsCurrent = receiptIsCurrent;
|
|
10
10
|
exports.routingDiagnostic = routingDiagnostic;
|
|
11
|
+
exports.storedCapabilityPath = storedCapabilityPath;
|
|
11
12
|
exports.capabilityReceiptPath = capabilityReceiptPath;
|
|
12
13
|
exports.readCapabilities = readCapabilities;
|
|
13
14
|
exports.approveCapabilities = approveCapabilities;
|
|
@@ -107,7 +108,12 @@ function validateCapabilityReceipt(value) {
|
|
|
107
108
|
}
|
|
108
109
|
function receiptIsCurrent(receipt, runtime, now) { return receipt.runtime.target === runtime.target && receipt.runtime.kind === runtime.kind && receipt.runtime.version === runtime.version && receipt.runtime.accountScopeDigest === runtime.accountScopeDigest && Number.isFinite(now.getTime()) && Date.parse(receipt.recordedAt) <= now.getTime() && now.getTime() <= Date.parse(receipt.expiresAt); }
|
|
109
110
|
function routingDiagnostic(code, message) { return { code, message }; }
|
|
110
|
-
function
|
|
111
|
+
function storedCapabilityPath(target, kind) {
|
|
112
|
+
if (typeof target !== 'string' || !providers_1.AGENT_TARGETS.includes(target) || typeof kind !== 'string' || !RUNTIME_KIND.test(kind))
|
|
113
|
+
throw new Error('stored capability target or kind is invalid');
|
|
114
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'routing-capabilities', target, `${kind}.json`);
|
|
115
|
+
}
|
|
116
|
+
function capabilityReceiptPath(runtime) { const valid = validateRuntimeKey(runtime); return storedCapabilityPath(valid.target, valid.kind); }
|
|
111
117
|
function safeParents(file) { for (let dir = path_1.default.dirname(path_1.default.resolve(file));; dir = path_1.default.dirname(dir)) {
|
|
112
118
|
try {
|
|
113
119
|
const stat = fs_1.default.lstatSync(dir);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.recordClaudeHookStart = recordClaudeHookStart;
|
|
7
|
+
exports.readClaudeHookPair = readClaudeHookPair;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const paths_1 = require("../paths");
|
|
12
|
+
const json_1 = require("../plan/json");
|
|
13
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
14
|
+
const machine_key_1 = require("./machine-key");
|
|
15
|
+
const native_claude_1 = require("./native-claude");
|
|
16
|
+
const DOMAIN = 'AWM Claude hook start/v1\0';
|
|
17
|
+
const MAX_BYTES = 8192;
|
|
18
|
+
function id(value, label) {
|
|
19
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value))
|
|
20
|
+
throw new Error(`${label} is invalid`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function event(value, kind) {
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.hook_event_name !== kind)
|
|
25
|
+
throw new Error(`Claude ${kind} hook is invalid`);
|
|
26
|
+
const row = value;
|
|
27
|
+
id(row.session_id, 'Claude session id');
|
|
28
|
+
id(row.agent_id, 'Claude agent id');
|
|
29
|
+
if (typeof row.agent_type !== 'string' || row.agent_type.length === 0 || row.agent_type.length > 128
|
|
30
|
+
|| /[\u0000-\u001f\u007f-\u009f]/.test(row.agent_type))
|
|
31
|
+
throw new Error('Claude agent type is invalid');
|
|
32
|
+
return row;
|
|
33
|
+
}
|
|
34
|
+
function fileFor(sessionId, agentId) {
|
|
35
|
+
const digest = (0, crypto_1.createHash)('sha256').update(`${sessionId}\0${agentId}`).digest('hex');
|
|
36
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'claude-hook-starts', `${digest}.json`);
|
|
37
|
+
}
|
|
38
|
+
function mac(value, key) { return (0, crypto_1.createHmac)('sha256', key).update(DOMAIN).update(JSON.stringify(value)).digest('hex'); }
|
|
39
|
+
function safeDirectory(dir, create) {
|
|
40
|
+
const chain = [];
|
|
41
|
+
for (let current = dir;; current = path_1.default.dirname(current)) {
|
|
42
|
+
chain.unshift(current);
|
|
43
|
+
if (current === path_1.default.dirname(current))
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
for (const item of chain) {
|
|
47
|
+
try {
|
|
48
|
+
const stat = fs_1.default.lstatSync(item);
|
|
49
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
50
|
+
throw new Error('Claude hook event has unsafe ancestor');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== 'ENOENT' || !create)
|
|
54
|
+
throw error;
|
|
55
|
+
fs_1.default.mkdirSync(item, { mode: 0o700 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Persist bounded hook identity only; never a prompt, transcript or account ID. */
|
|
60
|
+
function recordClaudeHookStart(value, now) {
|
|
61
|
+
const row = event(value, 'SubagentStart');
|
|
62
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
63
|
+
throw new Error('Claude hook clock is invalid');
|
|
64
|
+
const root = (0, paths_1.awmHome)();
|
|
65
|
+
const key = (0, machine_key_1.readMachineKey)(root);
|
|
66
|
+
if (!key)
|
|
67
|
+
throw new Error('Claude hook enrollment requires explicit machine setup');
|
|
68
|
+
const start = { hook_event_name: 'SubagentStart', session_id: row.session_id, agent_id: row.agent_id,
|
|
69
|
+
agent_type: row.agent_type, recorded_at: now.toISOString() };
|
|
70
|
+
const file = fileFor(start.session_id, start.agent_id);
|
|
71
|
+
native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
72
|
+
safeDirectory(path_1.default.dirname(file), true);
|
|
73
|
+
const content = Buffer.from(`${JSON.stringify({ start, mac: mac(start, key) })}\n`);
|
|
74
|
+
let observed = null;
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs_1.default.lstatSync(file);
|
|
77
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
78
|
+
throw new Error('Claude hook start is unsafe');
|
|
79
|
+
observed = native_bridge_1.secureFs.readRegularFile(file, MAX_BYTES);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code !== 'ENOENT')
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, path_1.default.relative(root, file).split(path_1.default.sep).join('/'), content, observed ? { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity } : { mode: 'create', createParents: false });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Link Stop to a sealed Start event on this machine. Pending events expire
|
|
89
|
+
* after one day; this is not the capability receipt lifetime. */
|
|
90
|
+
function readClaudeHookPair(stopValue, now) {
|
|
91
|
+
const stop = event(stopValue, 'SubagentStop');
|
|
92
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
93
|
+
throw new Error('Claude hook clock is invalid');
|
|
94
|
+
const key = (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
95
|
+
if (!key)
|
|
96
|
+
throw new Error('Claude hook machine key is missing');
|
|
97
|
+
const file = fileFor(stop.session_id, stop.agent_id);
|
|
98
|
+
safeDirectory(path_1.default.dirname(file), false);
|
|
99
|
+
const stat = fs_1.default.lstatSync(file);
|
|
100
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
101
|
+
throw new Error('Claude hook start is unsafe');
|
|
102
|
+
const parsed = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(native_bridge_1.secureFs.readRegularFile(file, MAX_BYTES).bytes));
|
|
103
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
104
|
+
throw new Error('Claude hook start envelope is invalid');
|
|
105
|
+
const envelope = parsed;
|
|
106
|
+
if (Object.keys(envelope).length !== 2 || typeof envelope.mac !== 'string' || !/^[a-f0-9]{64}$/.test(envelope.mac)
|
|
107
|
+
|| !envelope.start || typeof envelope.start !== 'object' || Array.isArray(envelope.start))
|
|
108
|
+
throw new Error('Claude hook start seal is invalid');
|
|
109
|
+
const start = envelope.start;
|
|
110
|
+
if (Object.keys(start).length !== 5 || start.hook_event_name !== 'SubagentStart' || typeof start.recorded_at !== 'string'
|
|
111
|
+
|| !Number.isFinite(Date.parse(start.recorded_at)) || start.session_id !== stop.session_id || start.agent_id !== stop.agent_id)
|
|
112
|
+
throw new Error('Claude hook start identity is invalid');
|
|
113
|
+
if (!(0, crypto_1.timingSafeEqual)(Buffer.from(envelope.mac, 'hex'), Buffer.from(mac(start, key), 'hex')))
|
|
114
|
+
throw new Error('Claude hook start seal does not match');
|
|
115
|
+
const ageMs = now.getTime() - Date.parse(start.recorded_at);
|
|
116
|
+
if (ageMs < 0 || ageMs > 24 * 60 * 60 * 1000)
|
|
117
|
+
throw new Error('Claude hook start is stale');
|
|
118
|
+
const lifecycle = (0, native_claude_1.normalizeClaudeSubagentLifecycle)(start, stop);
|
|
119
|
+
return { lifecycle, transcriptPath: stop.agent_transcript_path };
|
|
120
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.claudeAgentIdDigest = claudeAgentIdDigest;
|
|
7
|
+
exports.readStoredEventReceipt = readStoredEventReceipt;
|
|
8
|
+
exports.publishCodexObservation = publishCodexObservation;
|
|
9
|
+
exports.publishClaudeObservation = publishClaudeObservation;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const paths_1 = require("../paths");
|
|
14
|
+
const json_1 = require("../plan/json");
|
|
15
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
16
|
+
const capabilities_1 = require("./capabilities");
|
|
17
|
+
const capabilities_v2_1 = require("./capabilities-v2");
|
|
18
|
+
const native_codex_1 = require("./native-codex");
|
|
19
|
+
const native_claude_1 = require("./native-claude");
|
|
20
|
+
const native_claude_transcript_1 = require("./native-claude-transcript");
|
|
21
|
+
const native_claude_agent_definition_1 = require("./native-claude-agent-definition");
|
|
22
|
+
const machine_key_1 = require("./machine-key");
|
|
23
|
+
const validate_1 = require("./validate");
|
|
24
|
+
const MAX_RECEIPT_BYTES = 256 * 1024;
|
|
25
|
+
const SEAL_DOMAIN = 'AWM routing-capabilities/v2 local receipt\0';
|
|
26
|
+
const CLAUDE_AGENT_DOMAIN = 'AWM Claude native agent ID/v1\0';
|
|
27
|
+
function claudeAgentIdDigest(agentId, key) {
|
|
28
|
+
if (typeof agentId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(agentId) || !Buffer.isBuffer(key) || key.length !== 32)
|
|
29
|
+
throw new Error('Claude native agent identity is invalid');
|
|
30
|
+
return (0, crypto_1.createHmac)('sha256', key).update(CLAUDE_AGENT_DOMAIN).update(agentId).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
function receiptPath(runtime) {
|
|
33
|
+
const valid = (0, capabilities_1.validateRuntimeKey)(runtime);
|
|
34
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'routing-capabilities-v2', valid.target, `${valid.kind}.json`);
|
|
35
|
+
}
|
|
36
|
+
function safeParents(file, create) {
|
|
37
|
+
const chain = [];
|
|
38
|
+
for (let dir = path_1.default.dirname(file);; dir = path_1.default.dirname(dir)) {
|
|
39
|
+
chain.unshift(dir);
|
|
40
|
+
if (dir === path_1.default.dirname(dir))
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
for (const dir of chain) {
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
46
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
47
|
+
throw new Error('event receipt has unsafe or symlinked ancestor');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== 'ENOENT' || !create)
|
|
51
|
+
throw error;
|
|
52
|
+
fs_1.default.mkdirSync(dir, { mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readFile(file) {
|
|
57
|
+
try {
|
|
58
|
+
safeParents(file, false);
|
|
59
|
+
const stat = fs_1.default.lstatSync(file);
|
|
60
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
61
|
+
throw new Error('event receipt is unsafe or symlinked');
|
|
62
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, MAX_RECEIPT_BYTES).bytes;
|
|
63
|
+
const parsed = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
64
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
65
|
+
throw new Error('event receipt envelope is invalid');
|
|
66
|
+
const envelope = parsed;
|
|
67
|
+
if (Object.keys(envelope).length !== 2 || !Object.keys(envelope).every(key => ['receipt', 'mac'].includes(key))
|
|
68
|
+
|| typeof envelope.mac !== 'string' || !/^[a-f0-9]{64}$/.test(envelope.mac))
|
|
69
|
+
throw new Error('event receipt seal is invalid');
|
|
70
|
+
const receipt = (0, capabilities_v2_1.validateEventReceipt)(envelope.receipt);
|
|
71
|
+
const key = (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)());
|
|
72
|
+
if (!key)
|
|
73
|
+
throw new Error('event receipt machine key is missing');
|
|
74
|
+
const expected = seal(receipt, key);
|
|
75
|
+
if (!(0, crypto_1.timingSafeEqual)(Buffer.from(envelope.mac, 'hex'), Buffer.from(expected, 'hex')))
|
|
76
|
+
throw new Error('event receipt seal does not match this machine');
|
|
77
|
+
return receipt;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code === 'ENOENT')
|
|
81
|
+
return null;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function seal(receipt, key) {
|
|
86
|
+
return (0, crypto_1.createHmac)('sha256', key).update(SEAL_DOMAIN).update(JSON.stringify(receipt)).digest('hex');
|
|
87
|
+
}
|
|
88
|
+
function readStoredEventReceipt(runtime) {
|
|
89
|
+
try {
|
|
90
|
+
const value = readFile(receiptPath(runtime));
|
|
91
|
+
return value === null ? { state: 'absent' } : { state: 'present', receipt: value };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return { state: 'invalid', reason: error.message.slice(0, 4096) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function sameSelection(left, right) {
|
|
98
|
+
return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind
|
|
99
|
+
&& (left.effort.kind !== 'explicit' || left.effort.value === right.effort.value);
|
|
100
|
+
}
|
|
101
|
+
function sameScope(left, right) {
|
|
102
|
+
return left.runtime.target === right.runtime.target && left.runtime.kind === right.runtime.kind && left.runtime.version === right.runtime.version
|
|
103
|
+
&& left.runtime.accountScopeDigest === right.runtime.accountScopeDigest && left.binaryDigest === right.binaryDigest && left.configDigest === right.configDigest;
|
|
104
|
+
}
|
|
105
|
+
/** The only Codex v2 writer takes a native observation, never a candidate JSON.
|
|
106
|
+
* The CLI must call the native adapter and derive scope from this machine. */
|
|
107
|
+
function publishCodexObservation(input) {
|
|
108
|
+
if (!input || typeof input !== 'object' || !(0, native_codex_1.isNativeCodexChildObservation)(input.observation))
|
|
109
|
+
throw new Error('Codex native provenance is required');
|
|
110
|
+
if (input.observation.provenance !== 'codex-app-server-thread-read'
|
|
111
|
+
|| input.observation.nativeDispatchVerified !== true || input.observation.acceptedSelectionVerified !== true
|
|
112
|
+
|| input.observation.actualModelVerified !== false || input.observation.tokenUsage !== 'unknown')
|
|
113
|
+
throw new Error('Codex native selection is unverified');
|
|
114
|
+
const scope = input.scope;
|
|
115
|
+
if (!scope || scope.runtime?.target !== 'codex' || input.observation.childRuntimeVersion !== scope.runtime.version)
|
|
116
|
+
throw new Error('Codex native runtime version mismatch');
|
|
117
|
+
if (!sameSelection(input.observation.configuredSelection, input.expectedSelection))
|
|
118
|
+
throw new Error('Codex observed selection does not match approved selection');
|
|
119
|
+
(0, validate_1.assertDigest)(input.mappingDigest, 'mappingDigest');
|
|
120
|
+
if (!(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
|
|
121
|
+
throw new Error('event receipt clock is invalid');
|
|
122
|
+
const recordedAt = input.now.toISOString();
|
|
123
|
+
if (typeof input.observation.observedAt !== 'string' || !Number.isFinite(Date.parse(input.observation.observedAt)))
|
|
124
|
+
throw new Error('Codex native turn timestamp is unavailable');
|
|
125
|
+
const eventDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify({ parent: input.observation.parentThreadId, child: input.observation.childThreadId,
|
|
126
|
+
turn: input.observation.turnId, selection: input.observation.configuredSelection, runtimeVersion: input.observation.childRuntimeVersion,
|
|
127
|
+
accountScopeDigest: scope.runtime.accountScopeDigest })).digest('hex');
|
|
128
|
+
const claim = { selection: input.expectedSelection, mappingDigest: input.mappingDigest, eventDigest,
|
|
129
|
+
source: 'codex-turn-context', observedAt: input.observation.observedAt, actualModel: 'unverified', tokenUsage: 'unknown' };
|
|
130
|
+
const initial = (0, capabilities_v2_1.validateEventReceipt)({ schema: 'routing-capabilities/v2', ...scope, recordedAt, claims: [claim] });
|
|
131
|
+
const file = receiptPath(initial.runtime);
|
|
132
|
+
const root = (0, paths_1.awmHome)();
|
|
133
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
134
|
+
const key = (0, machine_key_1.readMachineKey)(root);
|
|
135
|
+
if (!key)
|
|
136
|
+
throw new Error('event receipt machine key is missing; run explicit setup');
|
|
137
|
+
safeParents(file, true);
|
|
138
|
+
const current = readFile(file);
|
|
139
|
+
if (current && sameScope(current, scope)) {
|
|
140
|
+
const prior = current.claims.find(item => sameSelection(item.selection, claim.selection));
|
|
141
|
+
if (prior && prior.mappingDigest === claim.mappingDigest && prior.eventDigest === claim.eventDigest)
|
|
142
|
+
return current;
|
|
143
|
+
}
|
|
144
|
+
const ageMs = input.now.getTime() - Date.parse(input.observation.observedAt);
|
|
145
|
+
if (ageMs < 0 || ageMs > 15 * 60 * 1000)
|
|
146
|
+
throw new Error('Codex native turn is not fresh for enrollment');
|
|
147
|
+
const claims = current && sameScope(current, scope) ? current.claims.filter(item => !sameSelection(item.selection, claim.selection)).concat(claim) : [claim];
|
|
148
|
+
const next = (0, capabilities_v2_1.validateEventReceipt)({ ...initial, claims });
|
|
149
|
+
const destination = path_1.default.relative(root, file).split(path_1.default.sep).join('/');
|
|
150
|
+
const bytes = Buffer.from(`${JSON.stringify({ receipt: next, mac: seal(next, key) }, null, 2)}\n`);
|
|
151
|
+
if (current) {
|
|
152
|
+
const observed = native_bridge_1.secureFs.readRegularFile(file, MAX_RECEIPT_BYTES);
|
|
153
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity });
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'create', createParents: false });
|
|
157
|
+
return next;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Bind a native hook pair to the provider's assistant transcript. The transcript
|
|
161
|
+
* reports a model, not effective effort or paid usage, so only runtime-default
|
|
162
|
+
* effort may be enrolled and usage remains unknown. */
|
|
163
|
+
function publishClaudeObservation(input) {
|
|
164
|
+
if (!input || typeof input !== 'object' || !(0, native_claude_1.isNativeClaudeSubagentLifecycle)(input.lifecycle)
|
|
165
|
+
|| !(0, native_claude_transcript_1.isNativeClaudeTranscriptObservation)(input.transcript))
|
|
166
|
+
throw new Error('Claude native hook and transcript provenance are required');
|
|
167
|
+
if (!(0, native_claude_agent_definition_1.isNativeClaudeAgentDeclaration)(input.declaredAgent) || input.declaredAgent.agentType !== input.lifecycle.agentType
|
|
168
|
+
|| input.declaredAgent.modelId !== input.transcript.actualModel)
|
|
169
|
+
throw new Error('Claude declared agent model provenance is required');
|
|
170
|
+
if (input.scope?.runtime?.target !== 'claude-code' || input.lifecycle.agentId !== input.transcript.agentId
|
|
171
|
+
|| input.expectedSelection?.selector?.kind !== 'model' || input.expectedSelection.selector.id !== input.transcript.actualModel
|
|
172
|
+
|| input.expectedSelection.effort.kind !== 'runtime-default')
|
|
173
|
+
throw new Error('Claude native selection does not match approved model or effort');
|
|
174
|
+
(0, validate_1.assertDigest)(input.mappingDigest, 'mappingDigest');
|
|
175
|
+
if (!(input.now instanceof Date) || !Number.isFinite(input.now.getTime()))
|
|
176
|
+
throw new Error('event receipt clock is invalid');
|
|
177
|
+
const eventDigest = (0, crypto_1.createHash)('sha256').update(JSON.stringify({ session: input.lifecycle.sessionId, agent: input.lifecycle.agentId,
|
|
178
|
+
transcriptDigest: input.transcript.evidenceDigest, accountScopeDigest: input.scope.runtime.accountScopeDigest })).digest('hex');
|
|
179
|
+
const nativeAgentIdDigest = claudeAgentIdDigest(input.lifecycle.agentId, (0, machine_key_1.readMachineKey)((0, paths_1.awmHome)()));
|
|
180
|
+
const nativeEvent = { nativeAgentIdDigest, eventDigest, observedAt: input.transcript.observedAt };
|
|
181
|
+
const claim = { selection: input.expectedSelection, mappingDigest: input.mappingDigest, eventDigest,
|
|
182
|
+
nativeAgentType: input.lifecycle.agentType,
|
|
183
|
+
nativeAgentIdDigest, nativeEvents: [nativeEvent],
|
|
184
|
+
source: 'claude-assistant-transcript', observedAt: input.transcript.observedAt,
|
|
185
|
+
actualModel: { id: input.transcript.actualModel, evidenceDigest: input.transcript.evidenceDigest }, tokenUsage: 'unknown' };
|
|
186
|
+
const initial = (0, capabilities_v2_1.validateEventReceipt)({ schema: 'routing-capabilities/v2', ...input.scope, recordedAt: input.now.toISOString(), claims: [claim] });
|
|
187
|
+
const file = receiptPath(initial.runtime);
|
|
188
|
+
const root = (0, paths_1.awmHome)();
|
|
189
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
190
|
+
const key = (0, machine_key_1.readMachineKey)(root);
|
|
191
|
+
if (!key)
|
|
192
|
+
throw new Error('event receipt machine key is missing; run explicit setup');
|
|
193
|
+
safeParents(file, true);
|
|
194
|
+
const current = readFile(file);
|
|
195
|
+
if (current && sameScope(current, input.scope)) {
|
|
196
|
+
const prior = current.claims.find(item => sameSelection(item.selection, claim.selection));
|
|
197
|
+
if (prior && prior.mappingDigest === claim.mappingDigest && prior.eventDigest === claim.eventDigest)
|
|
198
|
+
return current;
|
|
199
|
+
}
|
|
200
|
+
const ageMs = input.now.getTime() - Date.parse(input.transcript.observedAt);
|
|
201
|
+
if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > 15 * 60 * 1000)
|
|
202
|
+
throw new Error('Claude native transcript is not fresh for enrollment');
|
|
203
|
+
const prior = current && sameScope(current, input.scope) ? current.claims.find(item => sameSelection(item.selection, claim.selection)) : undefined;
|
|
204
|
+
const previousEvents = prior?.nativeEvents ?? (prior?.nativeAgentIdDigest ? [{ nativeAgentIdDigest: prior.nativeAgentIdDigest,
|
|
205
|
+
eventDigest: prior.eventDigest, observedAt: prior.observedAt }] : []);
|
|
206
|
+
const recentEvents = previousEvents.filter(item => input.now.getTime() - Date.parse(item.observedAt) <= 15 * 60_000
|
|
207
|
+
&& item.nativeAgentIdDigest !== nativeAgentIdDigest && item.eventDigest !== eventDigest);
|
|
208
|
+
const mergedClaim = { ...claim, nativeEvents: recentEvents.concat(nativeEvent).slice(-64) };
|
|
209
|
+
const claims = current && sameScope(current, input.scope) ? current.claims.filter(item => !sameSelection(item.selection, claim.selection)).concat(mergedClaim) : [mergedClaim];
|
|
210
|
+
const next = (0, capabilities_v2_1.validateEventReceipt)({ ...initial, claims });
|
|
211
|
+
const destination = path_1.default.relative(root, file).split(path_1.default.sep).join('/');
|
|
212
|
+
const bytes = Buffer.from(`${JSON.stringify({ receipt: next, mac: seal(next, key) }, null, 2)}\n`);
|
|
213
|
+
if (current) {
|
|
214
|
+
const observed = native_bridge_1.secureFs.readRegularFile(file, MAX_RECEIPT_BYTES);
|
|
215
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity });
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, destination, bytes, { mode: 'create', createParents: false });
|
|
219
|
+
return next;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.recordRoutingHookStatus = recordRoutingHookStatus;
|
|
7
|
+
exports.readRoutingHookStatus = readRoutingHookStatus;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const paths_1 = require("../paths");
|
|
11
|
+
const json_1 = require("../plan/json");
|
|
12
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
13
|
+
const fresh = () => ({ schema: 'routing-hook-status/v1', start: { count: 0 }, stop: { count: 0 } });
|
|
14
|
+
function target() { return path_1.default.join((0, paths_1.awmHome)(), 'routing-hook-status.json'); }
|
|
15
|
+
function safeParents(file) {
|
|
16
|
+
const chain = [];
|
|
17
|
+
for (let dir = path_1.default.dirname(file);; dir = path_1.default.dirname(dir)) {
|
|
18
|
+
chain.unshift(dir);
|
|
19
|
+
if (dir === path_1.default.dirname(dir))
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
for (const dir of chain) {
|
|
23
|
+
const stat = fs_1.default.lstatSync(dir);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
25
|
+
throw new Error('routing hook status has an unsafe ancestor');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function read() {
|
|
29
|
+
const file = target();
|
|
30
|
+
safeParents(file);
|
|
31
|
+
let stat;
|
|
32
|
+
try {
|
|
33
|
+
stat = fs_1.default.lstatSync(file);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'ENOENT')
|
|
37
|
+
return { status: fresh(), observed: null };
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
41
|
+
throw new Error('routing hook status is unsafe');
|
|
42
|
+
const observed = native_bridge_1.secureFs.readRegularFile(file, 8192);
|
|
43
|
+
const value = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(observed.bytes));
|
|
44
|
+
if (!value || value.schema !== 'routing-hook-status/v1' || !value.start || !value.stop
|
|
45
|
+
|| !Number.isSafeInteger(value.start.count) || !Number.isSafeInteger(value.stop.count)
|
|
46
|
+
|| value.start.count < 0 || value.stop.count < 0)
|
|
47
|
+
throw new Error('routing hook status is corrupt');
|
|
48
|
+
return { status: value, observed };
|
|
49
|
+
}
|
|
50
|
+
/** Failure is a redacted code, never provider payload or an exception string.
|
|
51
|
+
* Returns true only for a newly alertable failure episode. */
|
|
52
|
+
function recordRoutingHookStatus(phase, result, now = new Date()) {
|
|
53
|
+
if (phase !== 'start' && phase !== 'stop')
|
|
54
|
+
throw new Error('routing hook phase is invalid');
|
|
55
|
+
if (result !== 'success' && !/^[A-Z0-9_]{3,64}$/.test(result))
|
|
56
|
+
throw new Error('routing hook reason code is invalid');
|
|
57
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
58
|
+
throw new Error('routing hook clock is invalid');
|
|
59
|
+
const root = (0, paths_1.awmHome)();
|
|
60
|
+
return native_bridge_1.secureFs.withProjectLease(root, () => {
|
|
61
|
+
const { status, observed } = read();
|
|
62
|
+
const item = status[phase];
|
|
63
|
+
const pending = item.lastFailureAt !== undefined && (item.lastSuccessAt === undefined || item.lastFailureAt > item.lastSuccessAt);
|
|
64
|
+
const alert = result !== 'success' && (!pending || item.reasonCode !== result);
|
|
65
|
+
if (result === 'success')
|
|
66
|
+
item.lastSuccessAt = now.toISOString();
|
|
67
|
+
else {
|
|
68
|
+
item.lastFailureAt = now.toISOString();
|
|
69
|
+
item.reasonCode = result;
|
|
70
|
+
item.count += 1;
|
|
71
|
+
}
|
|
72
|
+
const bytes = Buffer.from(`${JSON.stringify(status)}\n`);
|
|
73
|
+
native_bridge_1.secureFs.writeProjectTransaction(root, path_1.default.basename(target()), bytes, observed
|
|
74
|
+
? { mode: 'replace', createParents: false, expected: observed.bytes, expectedIdentity: observed.identity }
|
|
75
|
+
: { mode: 'create', createParents: false });
|
|
76
|
+
return alert;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function readRoutingHookStatus() {
|
|
80
|
+
try {
|
|
81
|
+
const { status, observed } = read();
|
|
82
|
+
if (!observed)
|
|
83
|
+
return { state: 'absent' };
|
|
84
|
+
const failures = ['start', 'stop'].flatMap(phase => {
|
|
85
|
+
const item = status[phase];
|
|
86
|
+
return item.lastFailureAt && item.reasonCode && (!item.lastSuccessAt || item.lastFailureAt > item.lastSuccessAt)
|
|
87
|
+
? [{ phase, reasonCode: item.reasonCode, count: item.count }] : [];
|
|
88
|
+
});
|
|
89
|
+
return failures.length ? { state: 'failed', failures } : { state: 'healthy' };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { state: 'invalid' };
|
|
93
|
+
}
|
|
94
|
+
}
|