@open-agent-toolkit/cli 0.2.51 → 0.2.52
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/bootstrap.md +6 -2
- package/assets/docs/cli-utilities/tool-packs.md +87 -33
- package/assets/docs/provider-sync/commands.md +30 -2
- package/assets/docs/provider-sync/config.md +19 -0
- package/assets/docs/provider-sync/manifest-and-drift.md +37 -0
- package/assets/docs/provider-sync/providers.md +69 -2
- package/assets/docs/provider-sync/scope-and-surface.md +44 -0
- package/assets/docs/reference/cli-reference.md +1 -0
- package/assets/docs/reference/oat-directory-structure.md +11 -4
- package/assets/docs/workflows/projects/evidence-layers.md +110 -1
- package/assets/docs/workflows/projects/implementation-execution.md +13 -0
- package/assets/docs/workflows/projects/orchestration-model.md +18 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +57 -1
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +77 -1
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +24 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-plan-writing/SKILL.md +12 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-review-provide/SKILL.md +12 -1
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +12 -1
- package/dist/agents/canonical/index.d.ts +2 -0
- package/dist/agents/canonical/index.d.ts.map +1 -1
- package/dist/agents/canonical/index.js +1 -0
- package/dist/agents/canonical/resolve.d.ts +45 -0
- package/dist/agents/canonical/resolve.d.ts.map +1 -0
- package/dist/agents/canonical/resolve.js +175 -0
- package/dist/commands/decision/agents-guidance.d.ts +2 -2
- package/dist/commands/decision/agents-guidance.d.ts.map +1 -1
- package/dist/commands/decision/agents-guidance.js +1 -4
- package/dist/commands/decision/index.d.ts.map +1 -1
- package/dist/commands/decision/index.js +25 -6
- package/dist/commands/docs/init/index.d.ts +7 -1
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +53 -20
- package/dist/commands/doctor/index.d.ts +8 -2
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +157 -46
- package/dist/commands/init/index.d.ts +8 -3
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +64 -26
- package/dist/commands/init/tools/index.d.ts +12 -12
- package/dist/commands/init/tools/index.d.ts.map +1 -1
- package/dist/commands/init/tools/index.js +430 -99
- package/dist/commands/init/tools/install-state.d.ts +2 -0
- package/dist/commands/init/tools/install-state.d.ts.map +1 -1
- package/dist/commands/init/tools/install-state.js +11 -0
- package/dist/commands/init/tools/project-guidance.d.ts +49 -0
- package/dist/commands/init/tools/project-guidance.d.ts.map +1 -0
- package/dist/commands/init/tools/project-guidance.js +144 -0
- package/dist/commands/init/tools/workflows/index.d.ts +7 -0
- package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
- package/dist/commands/init/tools/workflows/index.js +160 -7
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +28 -3
- package/dist/commands/pjm/init.d.ts +5 -0
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +19 -3
- package/dist/commands/project/dispatch/index.d.ts +10 -0
- package/dist/commands/project/dispatch/index.d.ts.map +1 -0
- package/dist/commands/project/dispatch/index.js +137 -0
- package/dist/commands/project/dispatch/record.d.ts +91 -0
- package/dist/commands/project/dispatch/record.d.ts.map +1 -0
- package/dist/commands/project/dispatch/record.js +402 -0
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/providers/inspect/inspect.d.ts +8 -1
- package/dist/commands/providers/inspect/inspect.d.ts.map +1 -1
- package/dist/commands/providers/inspect/inspect.js +146 -26
- package/dist/commands/providers/list/list.d.ts +8 -1
- package/dist/commands/providers/list/list.d.ts.map +1 -1
- package/dist/commands/providers/list/list.js +139 -23
- package/dist/commands/providers/providers.types.d.ts +25 -1
- package/dist/commands/providers/providers.types.d.ts.map +1 -1
- package/dist/commands/providers/set/index.d.ts +9 -1
- package/dist/commands/providers/set/index.d.ts.map +1 -1
- package/dist/commands/providers/set/index.js +43 -33
- package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
- package/dist/commands/remove/skill/remove-skill.js +11 -0
- package/dist/commands/shared/agents-md.d.ts +42 -14
- package/dist/commands/shared/agents-md.d.ts.map +1 -1
- package/dist/commands/shared/agents-md.js +364 -70
- package/dist/commands/status/index.d.ts +8 -3
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +147 -32
- package/dist/commands/sync/apply.d.ts.map +1 -1
- package/dist/commands/sync/apply.js +220 -29
- package/dist/commands/sync/dry-run.d.ts.map +1 -1
- package/dist/commands/sync/dry-run.js +30 -6
- package/dist/commands/sync/index.d.ts +17 -2
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +106 -41
- package/dist/commands/sync/sync.types.d.ts +39 -2
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/commands/sync/sync.utils.d.ts +5 -1
- package/dist/commands/sync/sync.utils.d.ts.map +1 -1
- package/dist/commands/sync/sync.utils.js +73 -0
- package/dist/commands/tools/info/info-tool.d.ts +2 -0
- package/dist/commands/tools/info/info-tool.d.ts.map +1 -1
- package/dist/commands/tools/info/info-tool.js +34 -12
- package/dist/commands/tools/install/index.d.ts.map +1 -1
- package/dist/commands/tools/install/index.js +19 -3
- package/dist/commands/tools/list/list-tools.d.ts +2 -0
- package/dist/commands/tools/list/list-tools.d.ts.map +1 -1
- package/dist/commands/tools/list/list-tools.js +36 -7
- package/dist/commands/tools/remove/index.d.ts.map +1 -1
- package/dist/commands/tools/remove/index.js +150 -21
- package/dist/commands/tools/remove/remove-tools.d.ts +20 -0
- package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
- package/dist/commands/tools/remove/remove-tools.js +122 -1
- package/dist/commands/tools/shared/format-pack-inventory.d.ts +19 -0
- package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/format-pack-inventory.js +109 -0
- package/dist/commands/tools/shared/pack-evidence.d.ts +66 -0
- package/dist/commands/tools/shared/pack-evidence.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-evidence.js +132 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts +1 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +44 -0
- package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -0
- package/dist/commands/tools/shared/pack-lifecycle-outcome.js +91 -0
- package/dist/commands/tools/update/index.d.ts.map +1 -1
- package/dist/commands/tools/update/index.js +74 -4
- package/dist/commands/tools/update/update-tools.d.ts +5 -0
- package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
- package/dist/commands/tools/update/update-tools.js +64 -2
- package/dist/config/oat-config.d.ts +5 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +17 -5
- package/dist/config/user-sync-config.d.ts +1 -0
- package/dist/config/user-sync-config.d.ts.map +1 -1
- package/dist/config/user-sync-config.js +8 -0
- package/dist/drift/detector.d.ts +2 -2
- package/dist/drift/detector.d.ts.map +1 -1
- package/dist/drift/detector.js +18 -0
- package/dist/drift/strays.d.ts.map +1 -1
- package/dist/drift/strays.js +5 -0
- package/dist/engine/collection-sync.d.ts +14 -0
- package/dist/engine/collection-sync.d.ts.map +1 -0
- package/dist/engine/collection-sync.js +282 -0
- package/dist/engine/compute-plan.d.ts +8 -1
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +303 -12
- package/dist/engine/engine.types.d.ts +61 -0
- package/dist/engine/engine.types.d.ts.map +1 -1
- package/dist/engine/execute-plan.d.ts +22 -2
- package/dist/engine/execute-plan.d.ts.map +1 -1
- package/dist/engine/execute-plan.js +459 -27
- package/dist/engine/provider-path-safety.d.ts +1 -0
- package/dist/engine/provider-path-safety.d.ts.map +1 -1
- package/dist/engine/provider-path-safety.js +15 -0
- package/dist/engine/scanner.d.ts +2 -2
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +21 -4
- package/dist/fs/io.d.ts +67 -0
- package/dist/fs/io.d.ts.map +1 -1
- package/dist/fs/io.js +536 -3
- package/dist/manifest/index.d.ts +2 -2
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/manager.d.ts +2 -2
- package/dist/manifest/manager.d.ts.map +1 -1
- package/dist/manifest/manager.js +20 -3
- package/dist/manifest/manifest.types.d.ts +704 -13
- package/dist/manifest/manifest.types.d.ts.map +1 -1
- package/dist/manifest/manifest.types.js +131 -15
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +41 -10
- package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/cursor/codec/sync-extension.js +41 -10
- package/dist/providers/identity/absolute-paths.d.ts +68 -0
- package/dist/providers/identity/absolute-paths.d.ts.map +1 -0
- package/dist/providers/identity/absolute-paths.js +144 -0
- package/dist/providers/identity/claude-runtime-observation.d.ts +105 -0
- package/dist/providers/identity/claude-runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +40 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -0
- package/dist/providers/identity/claude-runtime-observation.fixtures.js +146 -0
- package/dist/providers/identity/claude-runtime-observation.js +229 -0
- package/dist/providers/identity/codex-runtime-observation.d.ts +75 -0
- package/dist/providers/identity/codex-runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts +37 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts.map +1 -0
- package/dist/providers/identity/codex-runtime-observation.fixtures.js +539 -0
- package/dist/providers/identity/codex-runtime-observation.js +255 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts +313 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -0
- package/dist/providers/identity/generic-dispatch-record.js +539 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts +1964 -0
- package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -0
- package/dist/providers/identity/oat-dispatch-record.js +691 -0
- package/dist/providers/identity/runtime-observation.d.ts +100 -0
- package/dist/providers/identity/runtime-observation.d.ts.map +1 -0
- package/dist/providers/identity/runtime-observation.js +169 -0
- package/dist/providers/shared/index.d.ts +2 -0
- package/dist/providers/shared/index.d.ts.map +1 -1
- package/dist/providers/shared/index.js +1 -0
- package/dist/providers/shared/materialization-extension.d.ts +12 -0
- package/dist/providers/shared/materialization-extension.d.ts.map +1 -1
- package/dist/providers/shared/registry.d.ts +69 -0
- package/dist/providers/shared/registry.d.ts.map +1 -0
- package/dist/providers/shared/registry.js +252 -0
- package/dist/providers/shared/restart-adviser.d.ts +26 -0
- package/dist/providers/shared/restart-adviser.d.ts.map +1 -0
- package/dist/providers/shared/restart-adviser.js +96 -0
- package/dist/shared/types.d.ts +4 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { redactAbsolutePathsDeep } from './absolute-paths.js';
|
|
4
|
+
import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, genericDispatchRecordSchema, normalizeDispatchKey, parseGenericDispatchRecord, } from './generic-dispatch-record.js';
|
|
5
|
+
/**
|
|
6
|
+
* A pre-start native-selection rejection is a closed event set. It is proof
|
|
7
|
+
* that the native launch surface refused the exact target before any child
|
|
8
|
+
* started, and it is the only category that can authorize one canonical
|
|
9
|
+
* fallback. It is deliberately disjoint from terminal child-outcome codes.
|
|
10
|
+
*/
|
|
11
|
+
export const QUALIFYING_PRE_START_REJECTION_CODES = [
|
|
12
|
+
'native-role-unavailable',
|
|
13
|
+
'native-target-unavailable',
|
|
14
|
+
'native-selector-unsupported',
|
|
15
|
+
'native-catalog-unsatisfying',
|
|
16
|
+
'capability-unresolved-or-unsupported',
|
|
17
|
+
'wrapper-payload-rejected',
|
|
18
|
+
'wrapper-launch-refused',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Terminal or post-acceptance outcome families. None of these ever authorizes
|
|
22
|
+
* replacement, so they are rejected with an explicit diagnostic rather than
|
|
23
|
+
* falling through the generic closed-set message.
|
|
24
|
+
*/
|
|
25
|
+
const PROHIBITED_REJECTION_FAMILIES = [
|
|
26
|
+
{ term: 'timeout', category: 'timeout' },
|
|
27
|
+
{ term: 'timedout', category: 'timeout' },
|
|
28
|
+
{ term: 'deadlineexceeded', category: 'timeout' },
|
|
29
|
+
{ term: 'blocked', category: 'BLOCKED' },
|
|
30
|
+
{ term: 'refus', category: 'refusal' },
|
|
31
|
+
{ term: 'declin', category: 'refusal' },
|
|
32
|
+
{ term: 'interrupt', category: 'interruption' },
|
|
33
|
+
{ term: 'cancel', category: 'interruption' },
|
|
34
|
+
{ term: 'abort', category: 'interruption' },
|
|
35
|
+
{ term: 'mismatch', category: 'runtime mismatch' },
|
|
36
|
+
{ term: 'missingtelemetry', category: 'missing telemetry' },
|
|
37
|
+
{ term: 'notreported', category: 'missing telemetry' },
|
|
38
|
+
{ term: 'malformed', category: 'malformed output' },
|
|
39
|
+
{ term: 'postacceptance', category: 'post-acceptance' },
|
|
40
|
+
{ term: 'postlaunch', category: 'post-acceptance' },
|
|
41
|
+
{ term: 'poststart', category: 'post-acceptance' },
|
|
42
|
+
];
|
|
43
|
+
function prohibitedRejectionCategory(code) {
|
|
44
|
+
const normalized = normalizeDispatchKey(code);
|
|
45
|
+
return (PROHIBITED_REJECTION_FAMILIES.find(({ term }) => normalized.includes(term))
|
|
46
|
+
?.category ?? null);
|
|
47
|
+
}
|
|
48
|
+
const preStartRejectionCodeSchema = z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.superRefine((code, context) => {
|
|
52
|
+
// The closed qualifying set is authoritative. Family classification only
|
|
53
|
+
// supplies a better diagnostic for a code that is already not qualifying.
|
|
54
|
+
if (QUALIFYING_PRE_START_REJECTION_CODES.includes(code)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const prohibited = prohibitedRejectionCategory(code);
|
|
58
|
+
context.addIssue({
|
|
59
|
+
code: z.ZodIssueCode.custom,
|
|
60
|
+
message: prohibited === null
|
|
61
|
+
? `Pre-start rejection code must be one of the qualifying codes: ${QUALIFYING_PRE_START_REJECTION_CODES.join(', ')}.`
|
|
62
|
+
: `A ${prohibited} outcome is not a pre-start native-selection rejection and never authorizes fallback or replacement.`,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Immutable configured controls across a fallback link.
|
|
67
|
+
*
|
|
68
|
+
* The rule, so a newly added generic field has a default: a field is immutable
|
|
69
|
+
* when it describes the configured selection, the authorization for that
|
|
70
|
+
* selection, or the evidence that justified it. A field is mutable when it
|
|
71
|
+
* describes this record's own identity, how the work is briefed to its own
|
|
72
|
+
* child, its own launch lifecycle, or its own child's observed behaviour. When
|
|
73
|
+
* a new field is ambiguous, treat it as immutable — a false "controls
|
|
74
|
+
* preserved" claim is worse than a false mismatch.
|
|
75
|
+
*
|
|
76
|
+
* All 46 generic fields are accounted for. The 31 immutable ones are listed
|
|
77
|
+
* below. The 15 mutable ones, each with its reason:
|
|
78
|
+
* - `request_id`: the fallback is a distinct request and must have its own ID.
|
|
79
|
+
* - `caller`: the adapter layer that launches the fallback may differ.
|
|
80
|
+
* - `objective`: a canonical-instruction fallback necessarily rebriefs the work
|
|
81
|
+
* for a generic child rather than a resolved role.
|
|
82
|
+
* - `role_name`, `role_selector`: the fallback deliberately targets a generic
|
|
83
|
+
* worker; substituting the role is the whole point of the approximation, and
|
|
84
|
+
* the resolved role identity is bound separately through `roleInstructions`.
|
|
85
|
+
* - `candidates_considered`: the fallback evaluated a different candidate set.
|
|
86
|
+
* - `selection_reason`: constrained separately — a fallback must declare
|
|
87
|
+
* `pre-start-rejection`, which by definition differs from the trigger's.
|
|
88
|
+
* - `launch_status`, `child_outcome`: each record's own lifecycle state.
|
|
89
|
+
* - `runtime_confirmation`: observation of that record's own child. The
|
|
90
|
+
* fallback is a different child, so equality would be a false assertion.
|
|
91
|
+
* - `diagnostics`: per-record narration. A fallback legitimately explains why
|
|
92
|
+
* it exists.
|
|
93
|
+
* - `continuation_events`: continuation linkage belongs to the record that
|
|
94
|
+
* owns the handle.
|
|
95
|
+
* - `expected_output`, `verification_evidence`, `escalate_when`: the brief
|
|
96
|
+
* given to this record's own child, in the same class as `objective`.
|
|
97
|
+
*
|
|
98
|
+
* Included even though they are evidence rather than controls, because they
|
|
99
|
+
* record why the preserved selection was allowed:
|
|
100
|
+
* - `configured_invocation_evidence`: names the configuration that authorized
|
|
101
|
+
* the preserved route.
|
|
102
|
+
* - `catalog_snapshot`: names the catalog the selection was made against.
|
|
103
|
+
* - `guidance_reference`, `guidance_version`, `guidance_verified_at`,
|
|
104
|
+
* `guidance_status`: name the dated model guidance behind the selection and
|
|
105
|
+
* how fresh it was; a fallback restating `stale` as `fresh` would misstate
|
|
106
|
+
* provenance for a preserved target.
|
|
107
|
+
* - `classification_reason`: the rationale for `task_class`, which is itself a
|
|
108
|
+
* compared authorization control.
|
|
109
|
+
*/
|
|
110
|
+
export const IMMUTABLE_FALLBACK_CONTROL_FIELDS = [
|
|
111
|
+
'provider',
|
|
112
|
+
'selection_source',
|
|
113
|
+
'model_selector',
|
|
114
|
+
'model_selector_granularity',
|
|
115
|
+
'effort_selector',
|
|
116
|
+
'reasoning_mode_selector',
|
|
117
|
+
'service_tier_selector',
|
|
118
|
+
'selected_route',
|
|
119
|
+
'authority',
|
|
120
|
+
'authorization_scope',
|
|
121
|
+
'deadline_seconds',
|
|
122
|
+
'retry_limit',
|
|
123
|
+
'payload',
|
|
124
|
+
'fallback',
|
|
125
|
+
'catalog_snapshot',
|
|
126
|
+
'configured_invocation_evidence',
|
|
127
|
+
'guidance_reference',
|
|
128
|
+
'guidance_version',
|
|
129
|
+
'guidance_verified_at',
|
|
130
|
+
'guidance_status',
|
|
131
|
+
'dispatch_context',
|
|
132
|
+
'dispatch_policy',
|
|
133
|
+
'dispatch_ceiling',
|
|
134
|
+
'scope',
|
|
135
|
+
'action',
|
|
136
|
+
'role_class',
|
|
137
|
+
'task_class',
|
|
138
|
+
'model_class_floor',
|
|
139
|
+
'classification_source',
|
|
140
|
+
'classification_reason',
|
|
141
|
+
'floor_satisfaction',
|
|
142
|
+
];
|
|
143
|
+
/**
|
|
144
|
+
* The mutable complement, kept executable rather than only described above so a
|
|
145
|
+
* newly added generic field fails `covers every generic dispatch field` until
|
|
146
|
+
* somebody records a decision for it.
|
|
147
|
+
*/
|
|
148
|
+
export const MUTABLE_FALLBACK_CONTROL_FIELDS = [
|
|
149
|
+
'request_id',
|
|
150
|
+
'caller',
|
|
151
|
+
'objective',
|
|
152
|
+
'role_name',
|
|
153
|
+
'role_selector',
|
|
154
|
+
'candidates_considered',
|
|
155
|
+
'selection_reason',
|
|
156
|
+
'launch_status',
|
|
157
|
+
'child_outcome',
|
|
158
|
+
'runtime_confirmation',
|
|
159
|
+
'diagnostics',
|
|
160
|
+
'continuation_events',
|
|
161
|
+
'expected_output',
|
|
162
|
+
'verification_evidence',
|
|
163
|
+
'escalate_when',
|
|
164
|
+
];
|
|
165
|
+
export function canonicalEvidenceJson(value) {
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
return `[${value.map(canonicalEvidenceJson).join(',')}]`;
|
|
168
|
+
}
|
|
169
|
+
if (value !== null && typeof value === 'object') {
|
|
170
|
+
return `{${Object.entries(value)
|
|
171
|
+
.filter(([, entry]) => entry !== undefined)
|
|
172
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
173
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalEvidenceJson(entry)}`)
|
|
174
|
+
.join(',')}}`;
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify(value ?? null);
|
|
177
|
+
}
|
|
178
|
+
function controlProjection(record) {
|
|
179
|
+
return Object.fromEntries(IMMUTABLE_FALLBACK_CONTROL_FIELDS.map((field) => [
|
|
180
|
+
field,
|
|
181
|
+
record[field] ?? null,
|
|
182
|
+
]));
|
|
183
|
+
}
|
|
184
|
+
export function configuredControlDigest(record) {
|
|
185
|
+
return `sha256:${createHash('sha256')
|
|
186
|
+
.update(canonicalEvidenceJson(controlProjection(record)))
|
|
187
|
+
.digest('hex')}`;
|
|
188
|
+
}
|
|
189
|
+
function differingControlFields(left, right) {
|
|
190
|
+
const leftControls = controlProjection(left);
|
|
191
|
+
const rightControls = controlProjection(right);
|
|
192
|
+
return IMMUTABLE_FALLBACK_CONTROL_FIELDS.filter((field) => canonicalEvidenceJson(leftControls[field]) !==
|
|
193
|
+
canonicalEvidenceJson(rightControls[field]));
|
|
194
|
+
}
|
|
195
|
+
const redactedPathSchema = z
|
|
196
|
+
.string()
|
|
197
|
+
.regex(/^<(?:loaded|user|project)>\/agents\/[a-z0-9][a-z0-9_-]*\.md$/);
|
|
198
|
+
const candidateMissSchema = z
|
|
199
|
+
.object({
|
|
200
|
+
tier: z.enum(['loaded', 'user', 'project']),
|
|
201
|
+
candidate: redactedPathSchema,
|
|
202
|
+
outcome: z.enum([
|
|
203
|
+
'missing',
|
|
204
|
+
'broken-symlink',
|
|
205
|
+
'escaping-symlink',
|
|
206
|
+
'noncanonical-copy',
|
|
207
|
+
'wrong-target',
|
|
208
|
+
'invalid-role',
|
|
209
|
+
]),
|
|
210
|
+
})
|
|
211
|
+
.strict();
|
|
212
|
+
const resolvedRoleSchema = z
|
|
213
|
+
.object({
|
|
214
|
+
status: z.literal('resolved'),
|
|
215
|
+
dependency: z.string().min(1),
|
|
216
|
+
canonicalRole: z.string().min(1),
|
|
217
|
+
tier: z.enum(['loaded', 'user', 'project']),
|
|
218
|
+
validation: z.enum(['direct-canonical', 'exact-canonical-symlink']),
|
|
219
|
+
canonicalPath: redactedPathSchema,
|
|
220
|
+
selectedPath: redactedPathSchema,
|
|
221
|
+
roleVersion: z.string().min(1),
|
|
222
|
+
contentDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
223
|
+
candidateMisses: z.array(candidateMissSchema),
|
|
224
|
+
})
|
|
225
|
+
.strict();
|
|
226
|
+
const missingRoleSchema = z
|
|
227
|
+
.object({
|
|
228
|
+
status: z.literal('missing'),
|
|
229
|
+
dependency: z.string().min(1),
|
|
230
|
+
canonicalRole: z.string().min(1),
|
|
231
|
+
candidateMisses: z.array(candidateMissSchema),
|
|
232
|
+
recovery: z.array(z.object({ command: z.string().min(1) }).strict()).min(1),
|
|
233
|
+
})
|
|
234
|
+
.strict();
|
|
235
|
+
const canonicalRoleSchema = z.discriminatedUnion('status', [
|
|
236
|
+
resolvedRoleSchema,
|
|
237
|
+
missingRoleSchema,
|
|
238
|
+
]);
|
|
239
|
+
const rejectionSchema = z
|
|
240
|
+
.object({
|
|
241
|
+
code: preStartRejectionCodeSchema,
|
|
242
|
+
rejectedAt: z.string().datetime(),
|
|
243
|
+
provesNoChildStarted: z.literal(true),
|
|
244
|
+
})
|
|
245
|
+
.strict();
|
|
246
|
+
const exactTargetSchema = z
|
|
247
|
+
.object({
|
|
248
|
+
provider: z.string().min(1),
|
|
249
|
+
modelSelector: z.string().min(1).nullable(),
|
|
250
|
+
effortSelector: z.string().min(1).nullable(),
|
|
251
|
+
reasoningModeSelector: z.string().min(1).nullable(),
|
|
252
|
+
serviceTierSelector: z.string().min(1).nullable(),
|
|
253
|
+
selectedRoute: z.string().min(1),
|
|
254
|
+
})
|
|
255
|
+
.strict();
|
|
256
|
+
const fallbackDispatchSchema = z
|
|
257
|
+
.object({
|
|
258
|
+
status: z.literal('fallback-dispatch'),
|
|
259
|
+
triggerRequestId: z.string().min(1),
|
|
260
|
+
fallbackRequestId: z.string().min(1),
|
|
261
|
+
trigger: z.literal('pre-start-rejection'),
|
|
262
|
+
fallbackReason: z.string().min(1),
|
|
263
|
+
kind: z.literal('canonical-instruction-fresh-child'),
|
|
264
|
+
approximation: z.literal(true),
|
|
265
|
+
preservedTarget: exactTargetSchema,
|
|
266
|
+
rejection: rejectionSchema.extend({
|
|
267
|
+
source: z.literal('provider-wrapper'),
|
|
268
|
+
}),
|
|
269
|
+
roleInstructions: resolvedRoleSchema,
|
|
270
|
+
})
|
|
271
|
+
.strict();
|
|
272
|
+
const fallbackSchema = z.discriminatedUnion('status', [
|
|
273
|
+
z
|
|
274
|
+
.object({
|
|
275
|
+
status: z.literal('not-applicable'),
|
|
276
|
+
reason: z.string().min(1),
|
|
277
|
+
})
|
|
278
|
+
.strict(),
|
|
279
|
+
fallbackDispatchSchema,
|
|
280
|
+
]);
|
|
281
|
+
/**
|
|
282
|
+
* Observation values are provider-reported identifiers, not prose. They are
|
|
283
|
+
* bounded at the same 256-character caller-authored identifier limit the
|
|
284
|
+
* generic record uses, so a transcript body cannot ride in through an
|
|
285
|
+
* optional metadata field.
|
|
286
|
+
*/
|
|
287
|
+
const MAX_OBSERVATION_VALUE_LENGTH = 256;
|
|
288
|
+
/**
|
|
289
|
+
* Provider identifiers only. Neither `/` nor `:` appears in any observed model,
|
|
290
|
+
* effort, tier, role, lineage, or source value, and admitting them let absolute
|
|
291
|
+
* paths (`C:/Users/...`), relative path-ish values, and URLs through in some
|
|
292
|
+
* spellings while NFR1 only ever intended none. Excluding both separators
|
|
293
|
+
* closes every spelling at once rather than denying them one at a time.
|
|
294
|
+
*/
|
|
295
|
+
const OBSERVATION_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
296
|
+
/** The axes an observation and a configured invocation can be compared on. */
|
|
297
|
+
const COMPARED_OBSERVATION_AXES = [
|
|
298
|
+
'role',
|
|
299
|
+
'model',
|
|
300
|
+
'effort',
|
|
301
|
+
'serviceTier',
|
|
302
|
+
];
|
|
303
|
+
/**
|
|
304
|
+
* The single canonical observation-value validator. Both the metadata path and
|
|
305
|
+
* a caller-supplied observation resolve through it, so a value can never be
|
|
306
|
+
* stored on one path that would be refused on the other.
|
|
307
|
+
*/
|
|
308
|
+
export function observationIdentifier(value) {
|
|
309
|
+
if (typeof value !== 'string')
|
|
310
|
+
return null;
|
|
311
|
+
const trimmed = value.trim();
|
|
312
|
+
if (trimmed.length === 0 || trimmed.length > MAX_OBSERVATION_VALUE_LENGTH) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return OBSERVATION_IDENTIFIER_PATTERN.test(trimmed) ? trimmed : null;
|
|
316
|
+
}
|
|
317
|
+
const observationValue = () => z
|
|
318
|
+
.string()
|
|
319
|
+
.min(1)
|
|
320
|
+
.max(MAX_OBSERVATION_VALUE_LENGTH)
|
|
321
|
+
.regex(OBSERVATION_IDENTIFIER_PATTERN, 'Observation values must be provider identifiers.');
|
|
322
|
+
const runtimeObservationSchema = z.discriminatedUnion('status', [
|
|
323
|
+
z.object({ status: z.literal('not-reported') }).strict(),
|
|
324
|
+
z
|
|
325
|
+
.object({
|
|
326
|
+
status: z.literal('reported'),
|
|
327
|
+
provider: observationValue(),
|
|
328
|
+
childLineage: observationValue().optional(),
|
|
329
|
+
role: observationValue().optional(),
|
|
330
|
+
model: observationValue().optional(),
|
|
331
|
+
effort: observationValue().optional(),
|
|
332
|
+
serviceTier: observationValue().optional(),
|
|
333
|
+
source: observationValue(),
|
|
334
|
+
observedAt: z.string().datetime(),
|
|
335
|
+
match: z.enum(['matching', 'mismatching', 'not-comparable']),
|
|
336
|
+
/**
|
|
337
|
+
* The axes the verdict actually rests on. Optional so a record written
|
|
338
|
+
* before this field existed still parses, but always written now: a
|
|
339
|
+
* `matching` that rests on one incidental axis is a materially weaker
|
|
340
|
+
* claim than one resting on model and role, and a consumer reading only
|
|
341
|
+
* the scalar `match` cannot tell them apart.
|
|
342
|
+
*/
|
|
343
|
+
comparedAxes: z
|
|
344
|
+
.array(observationValue())
|
|
345
|
+
.max(COMPARED_OBSERVATION_AXES.length)
|
|
346
|
+
.optional(),
|
|
347
|
+
})
|
|
348
|
+
.strict(),
|
|
349
|
+
]);
|
|
350
|
+
/** The literal an axis carries when the provider exposes no selectable value. */
|
|
351
|
+
export const NOT_EXPOSED_OBSERVATION_VALUE = 'not-exposed';
|
|
352
|
+
/**
|
|
353
|
+
* Project the record's immutable configured selection axes for comparison.
|
|
354
|
+
*
|
|
355
|
+
* This is a read-only projection: it never mutates the record, and its output
|
|
356
|
+
* is only ever compared against, never copied into, an observation.
|
|
357
|
+
*/
|
|
358
|
+
export function configuredInvocationForObservation(record) {
|
|
359
|
+
const roles = [record.role_name, record.role_selector].filter((value) => typeof value === 'string' && value !== '');
|
|
360
|
+
return {
|
|
361
|
+
role: [...new Set(roles)],
|
|
362
|
+
model: record.model_selector,
|
|
363
|
+
effort: record.effort_selector,
|
|
364
|
+
serviceTier: record.service_tier_selector ?? null,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Values that name the absence of an axis rather than a runtime fact. They can
|
|
369
|
+
* never establish agreement or disagreement, so they are excluded from the
|
|
370
|
+
* comparable set instead of being compared as literal strings.
|
|
371
|
+
*/
|
|
372
|
+
const NON_COMPARABLE_OBSERVED_VALUES = new Set([
|
|
373
|
+
NOT_EXPOSED_OBSERVATION_VALUE,
|
|
374
|
+
'not-reported',
|
|
375
|
+
'unknown',
|
|
376
|
+
]);
|
|
377
|
+
function comparableValue(value) {
|
|
378
|
+
if (typeof value !== 'string')
|
|
379
|
+
return null;
|
|
380
|
+
const trimmed = value.trim();
|
|
381
|
+
if (trimmed === '')
|
|
382
|
+
return null;
|
|
383
|
+
return NON_COMPARABLE_OBSERVED_VALUES.has(trimmed.toLowerCase())
|
|
384
|
+
? null
|
|
385
|
+
: trimmed.toLowerCase();
|
|
386
|
+
}
|
|
387
|
+
function comparableValues(axis) {
|
|
388
|
+
const values = typeof axis === 'string' ? [axis] : (axis ?? []);
|
|
389
|
+
return values
|
|
390
|
+
.map(comparableValue)
|
|
391
|
+
.filter((value) => value !== null);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Compare observed metadata with the configured invocation.
|
|
395
|
+
*
|
|
396
|
+
* Only axes both sides report are compared. With no comparable axis the result
|
|
397
|
+
* is `not-comparable` rather than `matching`: silence is never agreement. A
|
|
398
|
+
* mismatch is evidence only — it never authorizes replacement, retry, or
|
|
399
|
+
* fallback.
|
|
400
|
+
*/
|
|
401
|
+
export function comparedObservationAxes(metadata, configured) {
|
|
402
|
+
return COMPARED_OBSERVATION_AXES.filter((axis) => comparableValue(metadata[axis]) !== null &&
|
|
403
|
+
comparableValues(configured?.[axis]).length > 0);
|
|
404
|
+
}
|
|
405
|
+
export function compareObservedRuntimeMetadata(metadata, configured) {
|
|
406
|
+
const compared = comparedObservationAxes(metadata, configured);
|
|
407
|
+
if (compared.length === 0)
|
|
408
|
+
return 'not-comparable';
|
|
409
|
+
const mismatched = compared.some((axis) => {
|
|
410
|
+
const observed = comparableValue(metadata[axis]);
|
|
411
|
+
const expected = comparableValues(configured?.[axis]);
|
|
412
|
+
return observed === null || !expected.includes(observed);
|
|
413
|
+
});
|
|
414
|
+
return mismatched ? 'mismatching' : 'matching';
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Build one source-qualified runtime observation.
|
|
418
|
+
*
|
|
419
|
+
* Absent, empty, or schema-invalid metadata yields `not-reported`. The
|
|
420
|
+
* configured invocation is only ever read for the comparison, so a parse
|
|
421
|
+
* failure cannot copy a requested value into observed state.
|
|
422
|
+
*/
|
|
423
|
+
export function buildRuntimeObservation(input) {
|
|
424
|
+
const metadata = input.metadata;
|
|
425
|
+
if (metadata === null)
|
|
426
|
+
return { status: 'not-reported' };
|
|
427
|
+
assertNoSensitiveDispatchContent(metadata, '<observation>');
|
|
428
|
+
const reported = {};
|
|
429
|
+
for (const axis of ['childLineage', ...COMPARED_OBSERVATION_AXES]) {
|
|
430
|
+
const value = metadata[axis];
|
|
431
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
432
|
+
reported[axis] = value;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (Object.keys(reported).length === 0)
|
|
436
|
+
return { status: 'not-reported' };
|
|
437
|
+
const candidate = {
|
|
438
|
+
status: 'reported',
|
|
439
|
+
provider: input.provider,
|
|
440
|
+
...reported,
|
|
441
|
+
source: input.source,
|
|
442
|
+
observedAt: input.observedAt,
|
|
443
|
+
match: compareObservedRuntimeMetadata(metadata, input.configured),
|
|
444
|
+
comparedAxes: comparedObservationAxes(metadata, input.configured),
|
|
445
|
+
};
|
|
446
|
+
const parsed = runtimeObservationSchema.safeParse(candidate);
|
|
447
|
+
return parsed.success ? parsed.data : { status: 'not-reported' };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The rejected trigger owns the single-fallback right. Publishing a fallback
|
|
451
|
+
* record requires this durable claim on the trigger first, so two concurrent
|
|
452
|
+
* callers cannot each observe "no fallback yet" and then create their own.
|
|
453
|
+
*/
|
|
454
|
+
const fallbackClaimSchema = z
|
|
455
|
+
.object({
|
|
456
|
+
fallbackRequestId: z.string().min(1),
|
|
457
|
+
claimedAt: z.string().datetime(),
|
|
458
|
+
})
|
|
459
|
+
.strict();
|
|
460
|
+
const oatRecordSchema = z
|
|
461
|
+
.object({
|
|
462
|
+
schemaVersion: z.literal(1),
|
|
463
|
+
canonicalRole: canonicalRoleSchema.nullable(),
|
|
464
|
+
preStartRejection: rejectionSchema.nullable(),
|
|
465
|
+
fallbackClaim: fallbackClaimSchema.nullable().default(null),
|
|
466
|
+
fallback: fallbackSchema,
|
|
467
|
+
runtimeObservation: runtimeObservationSchema,
|
|
468
|
+
})
|
|
469
|
+
.strict();
|
|
470
|
+
export const persistedOatDispatchRecordSchema = genericDispatchRecordSchema
|
|
471
|
+
.innerType()
|
|
472
|
+
.extend({ oat: oatRecordSchema })
|
|
473
|
+
.strict();
|
|
474
|
+
/**
|
|
475
|
+
* Strict observation parse. Used where a caller supplied the observation
|
|
476
|
+
* directly, so a malformed value is refused rather than quietly degraded to
|
|
477
|
+
* `not-reported` — absent evidence and a bad claim are different facts.
|
|
478
|
+
*/
|
|
479
|
+
export function parseRuntimeObservation(value) {
|
|
480
|
+
return runtimeObservationSchema.parse(value);
|
|
481
|
+
}
|
|
482
|
+
const oatDispatchEvidenceEventSchema = z.discriminatedUnion('kind', [
|
|
483
|
+
z
|
|
484
|
+
.object({
|
|
485
|
+
kind: z.literal('canonical-role-resolution'),
|
|
486
|
+
requestId: z.string().min(1),
|
|
487
|
+
source: z.literal('canonical-role-resolver'),
|
|
488
|
+
evidence: canonicalRoleSchema,
|
|
489
|
+
})
|
|
490
|
+
.strict(),
|
|
491
|
+
z
|
|
492
|
+
.object({
|
|
493
|
+
kind: z.literal('pre-start-rejection-attestation'),
|
|
494
|
+
requestId: z.string().min(1),
|
|
495
|
+
source: z.literal('provider-wrapper'),
|
|
496
|
+
expectedLaunchStatus: z.literal('blocked-before-start'),
|
|
497
|
+
rejection: rejectionSchema,
|
|
498
|
+
})
|
|
499
|
+
.strict(),
|
|
500
|
+
z
|
|
501
|
+
.object({
|
|
502
|
+
kind: z.literal('fallback-claim'),
|
|
503
|
+
requestId: z.string().min(1),
|
|
504
|
+
source: z.literal('provider-wrapper'),
|
|
505
|
+
claim: fallbackClaimSchema,
|
|
506
|
+
})
|
|
507
|
+
.strict(),
|
|
508
|
+
z
|
|
509
|
+
.object({
|
|
510
|
+
kind: z.literal('fallback-link'),
|
|
511
|
+
requestId: z.string().min(1),
|
|
512
|
+
source: z.literal('provider-wrapper'),
|
|
513
|
+
evidence: fallbackDispatchSchema,
|
|
514
|
+
})
|
|
515
|
+
.strict(),
|
|
516
|
+
z
|
|
517
|
+
.object({
|
|
518
|
+
kind: z.literal('runtime-observation'),
|
|
519
|
+
requestId: z.string().min(1),
|
|
520
|
+
source: z.literal('runtime-observer'),
|
|
521
|
+
observation: runtimeObservationSchema,
|
|
522
|
+
})
|
|
523
|
+
.strict(),
|
|
524
|
+
]);
|
|
525
|
+
function initialOatRecord() {
|
|
526
|
+
return {
|
|
527
|
+
schemaVersion: 1,
|
|
528
|
+
canonicalRole: null,
|
|
529
|
+
preStartRejection: null,
|
|
530
|
+
fallbackClaim: null,
|
|
531
|
+
fallback: { status: 'not-applicable', reason: 'No fallback recorded.' },
|
|
532
|
+
runtimeObservation: { status: 'not-reported' },
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function genericPart(record) {
|
|
536
|
+
const { oat: _oat, ...generic } = record;
|
|
537
|
+
return parseGenericDispatchRecord(generic);
|
|
538
|
+
}
|
|
539
|
+
function oatPart(record) {
|
|
540
|
+
return 'oat' in record
|
|
541
|
+
? oatRecordSchema.parse(record.oat)
|
|
542
|
+
: initialOatRecord();
|
|
543
|
+
}
|
|
544
|
+
function targetFor(record) {
|
|
545
|
+
return {
|
|
546
|
+
provider: record.provider,
|
|
547
|
+
modelSelector: record.model_selector,
|
|
548
|
+
effortSelector: record.effort_selector,
|
|
549
|
+
reasoningModeSelector: record.reasoning_mode_selector ?? null,
|
|
550
|
+
serviceTierSelector: record.service_tier_selector ?? null,
|
|
551
|
+
selectedRoute: record.selected_route,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function assertMatchingRequest(record, requestId) {
|
|
555
|
+
if (record.request_id !== requestId) {
|
|
556
|
+
throw new Error('OAT event request ID must match the generic record.');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
export function parsePersistedOatDispatchRecord(value) {
|
|
560
|
+
assertNoSensitiveDispatchContent(value);
|
|
561
|
+
assertBoundedDispatchRecordSize(value);
|
|
562
|
+
const parsed = persistedOatDispatchRecordSchema.parse(value);
|
|
563
|
+
const { oat, ...generic } = parsed;
|
|
564
|
+
const sanitized = {
|
|
565
|
+
...parseGenericDispatchRecord(generic),
|
|
566
|
+
// The namespaced augmentation is nested evidence throughout — rejection
|
|
567
|
+
// reasons, fallback narration, observation values — so it is redacted
|
|
568
|
+
// rather than rejected. The canonical `<tier>/agents/<role>.md` role form
|
|
569
|
+
// is not an absolute path and survives untouched.
|
|
570
|
+
oat: oatRecordSchema.parse(redactAbsolutePathsDeep(oat)),
|
|
571
|
+
};
|
|
572
|
+
// Same ordering rule as the generic record: the size check above ran on the
|
|
573
|
+
// untransformed input, so it is re-run on the redacted result.
|
|
574
|
+
assertBoundedDispatchRecordSize(sanitized);
|
|
575
|
+
return sanitized;
|
|
576
|
+
}
|
|
577
|
+
export function augmentDispatchRecord(input) {
|
|
578
|
+
assertNoSensitiveDispatchContent(input.event);
|
|
579
|
+
const event = oatDispatchEvidenceEventSchema.parse(input.event);
|
|
580
|
+
const record = genericPart(input.record);
|
|
581
|
+
const oat = oatPart(input.record);
|
|
582
|
+
assertMatchingRequest(record, event.requestId);
|
|
583
|
+
switch (event.kind) {
|
|
584
|
+
case 'canonical-role-resolution': {
|
|
585
|
+
if (oat.canonicalRole !== null &&
|
|
586
|
+
JSON.stringify(oat.canonicalRole) !== JSON.stringify(event.evidence)) {
|
|
587
|
+
throw new Error('Canonical role evidence is immutable once recorded.');
|
|
588
|
+
}
|
|
589
|
+
oat.canonicalRole = canonicalRoleSchema.parse(event.evidence);
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case 'pre-start-rejection-attestation': {
|
|
593
|
+
if (event.expectedLaunchStatus !== 'blocked-before-start' ||
|
|
594
|
+
record.launch_status !== 'blocked-before-start') {
|
|
595
|
+
throw new Error('Pre-start rejection requires generic launch_status blocked-before-start.');
|
|
596
|
+
}
|
|
597
|
+
if (oat.preStartRejection !== null &&
|
|
598
|
+
JSON.stringify(oat.preStartRejection) !==
|
|
599
|
+
JSON.stringify(event.rejection)) {
|
|
600
|
+
throw new Error('Pre-start rejection evidence is immutable once recorded.');
|
|
601
|
+
}
|
|
602
|
+
oat.preStartRejection = rejectionSchema.parse(event.rejection);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case 'fallback-claim': {
|
|
606
|
+
const claim = event.claim;
|
|
607
|
+
if (record.launch_status !== 'blocked-before-start') {
|
|
608
|
+
throw new Error('Only a request blocked before start can claim a fallback.');
|
|
609
|
+
}
|
|
610
|
+
if (oat.preStartRejection === null) {
|
|
611
|
+
throw new Error('A fallback claim requires proven pre-start rejection evidence.');
|
|
612
|
+
}
|
|
613
|
+
if (oat.canonicalRole === null ||
|
|
614
|
+
oat.canonicalRole.status !== 'resolved') {
|
|
615
|
+
throw new Error('A fallback claim requires resolved canonical role evidence.');
|
|
616
|
+
}
|
|
617
|
+
if (oat.fallbackClaim !== null &&
|
|
618
|
+
oat.fallbackClaim.fallbackRequestId !== claim.fallbackRequestId) {
|
|
619
|
+
throw new Error('The rejected request already has a fallback.');
|
|
620
|
+
}
|
|
621
|
+
oat.fallbackClaim = claim;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'fallback-link': {
|
|
625
|
+
const trigger = input.triggerRecord
|
|
626
|
+
? parsePersistedOatDispatchRecord(input.triggerRecord)
|
|
627
|
+
: null;
|
|
628
|
+
if (!trigger) {
|
|
629
|
+
throw new Error('Fallback requires the rejected trigger record.');
|
|
630
|
+
}
|
|
631
|
+
const evidence = fallbackDispatchSchema.parse(event.evidence);
|
|
632
|
+
if (evidence.triggerRequestId !== trigger.request_id ||
|
|
633
|
+
evidence.fallbackRequestId !== record.request_id ||
|
|
634
|
+
trigger.launch_status !== 'blocked-before-start' ||
|
|
635
|
+
trigger.oat.preStartRejection === null ||
|
|
636
|
+
evidence.rejection.source !== 'provider-wrapper' ||
|
|
637
|
+
evidence.rejection.code !== trigger.oat.preStartRejection.code ||
|
|
638
|
+
evidence.rejection.rejectedAt !==
|
|
639
|
+
trigger.oat.preStartRejection.rejectedAt ||
|
|
640
|
+
evidence.rejection.provesNoChildStarted !== true) {
|
|
641
|
+
throw new Error('Fallback lacks matching pre-start rejection evidence.');
|
|
642
|
+
}
|
|
643
|
+
if (record.selection_reason !== 'pre-start-rejection') {
|
|
644
|
+
throw new Error('A fallback record must state selection_reason pre-start-rejection.');
|
|
645
|
+
}
|
|
646
|
+
if (canonicalEvidenceJson(evidence.preservedTarget) !==
|
|
647
|
+
canonicalEvidenceJson(targetFor(record)) ||
|
|
648
|
+
canonicalEvidenceJson(targetFor(trigger)) !==
|
|
649
|
+
canonicalEvidenceJson(targetFor(record))) {
|
|
650
|
+
throw new Error('Fallback must preserve the exact target and controls.');
|
|
651
|
+
}
|
|
652
|
+
const changedControls = differingControlFields(trigger, record);
|
|
653
|
+
if (changedControls.length > 0) {
|
|
654
|
+
throw new Error(`Fallback must preserve the exact target and controls; ${changedControls.join(', ')} changed.`);
|
|
655
|
+
}
|
|
656
|
+
if (configuredControlDigest(trigger) !== configuredControlDigest(record)) {
|
|
657
|
+
throw new Error('Fallback must preserve the exact target and controls; the configured control digest changed.');
|
|
658
|
+
}
|
|
659
|
+
if (trigger.oat.fallbackClaim === null ||
|
|
660
|
+
trigger.oat.fallbackClaim.fallbackRequestId !== record.request_id) {
|
|
661
|
+
throw new Error('The rejected trigger must durably claim this fallback request before the fallback can publish.');
|
|
662
|
+
}
|
|
663
|
+
const triggerRole = trigger.oat.canonicalRole;
|
|
664
|
+
if (triggerRole === null || triggerRole.status !== 'resolved') {
|
|
665
|
+
throw new Error('Fallback requires resolved canonical role evidence on the rejected trigger.');
|
|
666
|
+
}
|
|
667
|
+
if (canonicalEvidenceJson(evidence.roleInstructions) !==
|
|
668
|
+
canonicalEvidenceJson(triggerRole)) {
|
|
669
|
+
throw new Error("Fallback role evidence must equal the trigger's resolved canonical role evidence exactly.");
|
|
670
|
+
}
|
|
671
|
+
if ((input.relatedRecords ?? [])
|
|
672
|
+
.map(parsePersistedOatDispatchRecord)
|
|
673
|
+
.some((related) => related.oat.fallback.status === 'fallback-dispatch' &&
|
|
674
|
+
related.oat.fallback.triggerRequestId === trigger.request_id) ||
|
|
675
|
+
oat.fallback.status === 'fallback-dispatch') {
|
|
676
|
+
throw new Error('The rejected request already has a fallback.');
|
|
677
|
+
}
|
|
678
|
+
oat.fallback = evidence;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case 'runtime-observation':
|
|
682
|
+
if (oat.runtimeObservation.status === 'reported' &&
|
|
683
|
+
JSON.stringify(oat.runtimeObservation) !==
|
|
684
|
+
JSON.stringify(event.observation)) {
|
|
685
|
+
throw new Error('Runtime observation is immutable once reported.');
|
|
686
|
+
}
|
|
687
|
+
oat.runtimeObservation = runtimeObservationSchema.parse(event.observation);
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
return parsePersistedOatDispatchRecord({ ...record, oat });
|
|
691
|
+
}
|