@veil-runtime/core 0.1.3 → 0.3.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/README.md +58 -522
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/runtime/events/memory-event-bus.js +8 -1
- package/dist/runtime/execution/governed-value.d.ts +5 -0
- package/dist/runtime/execution/governed-value.js +198 -0
- package/dist/runtime/execution/plan-admission-error.d.ts +22 -0
- package/dist/runtime/execution/plan-admission-error.js +44 -0
- package/dist/runtime/execution/plan-validator.d.ts +3 -0
- package/dist/runtime/execution/plan-validator.js +23 -2
- package/dist/runtime/execution/result-reference.js +10 -3
- package/dist/runtime/jobs/job-manager.d.ts +3 -2
- package/dist/runtime/jobs/job-manager.js +97 -27
- package/dist/runtime/operator-runtime.d.ts +5 -7
- package/dist/runtime/operator-runtime.js +20 -1
- package/dist/runtime/registry/capability.d.ts +9 -0
- package/dist/runtime/registry/registry.d.ts +3 -8
- package/dist/runtime/registry/registry.js +22 -7
- package/package.json +4 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GovernedValueError = void 0;
|
|
4
|
+
exports.captureGovernedValue = captureGovernedValue;
|
|
5
|
+
exports.copyGovernedValue = copyGovernedValue;
|
|
6
|
+
const node_util_1 = require("node:util");
|
|
7
|
+
const MAX_DEPTH = 128;
|
|
8
|
+
const MAX_CONTAINERS = 10_000;
|
|
9
|
+
const MAX_EDGES = 100_000;
|
|
10
|
+
const MAX_STRING_UNITS = 1_048_576;
|
|
11
|
+
class GovernedValueError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super('Value is not a supported governed representation');
|
|
14
|
+
this.name = 'GovernedValueError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.GovernedValueError = GovernedValueError;
|
|
18
|
+
function reject() {
|
|
19
|
+
throw new GovernedValueError();
|
|
20
|
+
}
|
|
21
|
+
function primitive(value) {
|
|
22
|
+
if (value === null || value === undefined)
|
|
23
|
+
return true;
|
|
24
|
+
switch (typeof value) {
|
|
25
|
+
case 'boolean':
|
|
26
|
+
case 'string':
|
|
27
|
+
case 'number':
|
|
28
|
+
case 'bigint':
|
|
29
|
+
return true;
|
|
30
|
+
default:
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function excludedBrand(value) {
|
|
35
|
+
if (node_util_1.types.isProxy(value))
|
|
36
|
+
return true;
|
|
37
|
+
if (node_util_1.types.isDate(value) ||
|
|
38
|
+
node_util_1.types.isMap(value) ||
|
|
39
|
+
node_util_1.types.isSet(value) ||
|
|
40
|
+
node_util_1.types.isRegExp(value) ||
|
|
41
|
+
node_util_1.types.isBoxedPrimitive(value) ||
|
|
42
|
+
node_util_1.types.isAnyArrayBuffer(value) ||
|
|
43
|
+
node_util_1.types.isArrayBufferView(value) ||
|
|
44
|
+
node_util_1.types.isDataView(value) ||
|
|
45
|
+
node_util_1.types.isWeakMap(value) ||
|
|
46
|
+
node_util_1.types.isWeakSet(value) ||
|
|
47
|
+
node_util_1.types.isPromise(value))
|
|
48
|
+
return true;
|
|
49
|
+
const iteratorTypes = [
|
|
50
|
+
node_util_1.types.isMapIterator,
|
|
51
|
+
node_util_1.types.isSetIterator,
|
|
52
|
+
node_util_1.types.isGeneratorObject,
|
|
53
|
+
];
|
|
54
|
+
return iteratorTypes.some((check) => check?.(value) === true);
|
|
55
|
+
}
|
|
56
|
+
function inspect(value, counters) {
|
|
57
|
+
if (excludedBrand(value))
|
|
58
|
+
reject();
|
|
59
|
+
const prototype = Object.getPrototypeOf(value);
|
|
60
|
+
const isArray = Array.isArray(value);
|
|
61
|
+
if (isArray && prototype !== Array.prototype)
|
|
62
|
+
reject();
|
|
63
|
+
if (!isArray && prototype !== Object.prototype && prototype !== null)
|
|
64
|
+
reject();
|
|
65
|
+
const keys = Reflect.ownKeys(value);
|
|
66
|
+
const entries = [];
|
|
67
|
+
let length = 0;
|
|
68
|
+
if (isArray) {
|
|
69
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
70
|
+
if (!lengthDescriptor ||
|
|
71
|
+
lengthDescriptor.enumerable ||
|
|
72
|
+
!('value' in lengthDescriptor) ||
|
|
73
|
+
typeof lengthDescriptor.value !== 'number' ||
|
|
74
|
+
!Number.isSafeInteger(lengthDescriptor.value) ||
|
|
75
|
+
lengthDescriptor.value < 0)
|
|
76
|
+
reject();
|
|
77
|
+
length = lengthDescriptor.value;
|
|
78
|
+
}
|
|
79
|
+
for (const key of keys) {
|
|
80
|
+
if (typeof key !== 'string')
|
|
81
|
+
reject();
|
|
82
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
83
|
+
if (!descriptor || !('value' in descriptor))
|
|
84
|
+
reject();
|
|
85
|
+
if (isArray && key === 'length') {
|
|
86
|
+
if (descriptor.enumerable || typeof descriptor.value !== 'number' || descriptor.value !== length)
|
|
87
|
+
reject();
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!descriptor.enumerable)
|
|
91
|
+
reject();
|
|
92
|
+
if (typeof descriptor.value === 'string')
|
|
93
|
+
counters.stringUnits += descriptor.value.length;
|
|
94
|
+
counters.stringUnits += key.length;
|
|
95
|
+
if (counters.stringUnits > MAX_STRING_UNITS)
|
|
96
|
+
reject();
|
|
97
|
+
if (isArray) {
|
|
98
|
+
const index = Number(key);
|
|
99
|
+
if (!Number.isInteger(index) || index < 0 || String(index) !== key || index >= length)
|
|
100
|
+
reject();
|
|
101
|
+
}
|
|
102
|
+
entries.push({ key, value: descriptor.value });
|
|
103
|
+
}
|
|
104
|
+
if (isArray) {
|
|
105
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
106
|
+
reject();
|
|
107
|
+
for (let index = 0; index < length; index += 1) {
|
|
108
|
+
if (!entries.some((entry) => entry.key === String(index)))
|
|
109
|
+
reject();
|
|
110
|
+
}
|
|
111
|
+
counters.edges += entries.length;
|
|
112
|
+
if (counters.edges > MAX_EDGES)
|
|
113
|
+
reject();
|
|
114
|
+
return { target: new Array(length), entries };
|
|
115
|
+
}
|
|
116
|
+
counters.edges += entries.length;
|
|
117
|
+
if (counters.edges > MAX_EDGES)
|
|
118
|
+
reject();
|
|
119
|
+
return {
|
|
120
|
+
target: Object.create(prototype === null ? null : Object.prototype),
|
|
121
|
+
entries,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function clone(value, freeze) {
|
|
125
|
+
const counters = { containers: 0, edges: 0, stringUnits: 0 };
|
|
126
|
+
const memo = new Map();
|
|
127
|
+
const active = new Set();
|
|
128
|
+
const stack = [];
|
|
129
|
+
const make = (candidate, depth) => {
|
|
130
|
+
if (primitive(candidate)) {
|
|
131
|
+
if (typeof candidate === 'string') {
|
|
132
|
+
counters.stringUnits += candidate.length;
|
|
133
|
+
if (counters.stringUnits > MAX_STRING_UNITS)
|
|
134
|
+
reject();
|
|
135
|
+
}
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
139
|
+
reject();
|
|
140
|
+
if (depth > MAX_DEPTH)
|
|
141
|
+
reject();
|
|
142
|
+
const source = candidate;
|
|
143
|
+
const existing = memo.get(source);
|
|
144
|
+
if (existing) {
|
|
145
|
+
if (active.has(source))
|
|
146
|
+
reject();
|
|
147
|
+
return existing;
|
|
148
|
+
}
|
|
149
|
+
counters.containers += 1;
|
|
150
|
+
if (counters.containers > MAX_CONTAINERS)
|
|
151
|
+
reject();
|
|
152
|
+
const inspected = inspect(source, counters);
|
|
153
|
+
memo.set(source, inspected.target);
|
|
154
|
+
active.add(source);
|
|
155
|
+
stack.push({ source, target: inspected.target, entries: inspected.entries, index: 0 });
|
|
156
|
+
return inspected.target;
|
|
157
|
+
};
|
|
158
|
+
const root = make(value, 0);
|
|
159
|
+
while (stack.length > 0) {
|
|
160
|
+
const frame = stack[stack.length - 1];
|
|
161
|
+
if (frame.index >= frame.entries.length) {
|
|
162
|
+
active.delete(frame.source);
|
|
163
|
+
stack.pop();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const entry = frame.entries[frame.index++];
|
|
167
|
+
const child = make(entry.value, stack.length);
|
|
168
|
+
Object.defineProperty(frame.target, entry.key, {
|
|
169
|
+
value: child,
|
|
170
|
+
enumerable: true,
|
|
171
|
+
configurable: true,
|
|
172
|
+
writable: true,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (freeze && typeof root === 'object' && root !== null) {
|
|
176
|
+
const pending = [root];
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
while (pending.length > 0) {
|
|
179
|
+
const current = pending.pop();
|
|
180
|
+
if (seen.has(current))
|
|
181
|
+
continue;
|
|
182
|
+
seen.add(current);
|
|
183
|
+
for (const key of Object.keys(current)) {
|
|
184
|
+
const child = current[key];
|
|
185
|
+
if (typeof child === 'object' && child !== null)
|
|
186
|
+
pending.push(child);
|
|
187
|
+
}
|
|
188
|
+
Object.freeze(current);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return root;
|
|
192
|
+
}
|
|
193
|
+
function captureGovernedValue(value) {
|
|
194
|
+
return clone(value, false);
|
|
195
|
+
}
|
|
196
|
+
function copyGovernedValue(value, frozen = false) {
|
|
197
|
+
return clone(value, frozen);
|
|
198
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Structured admission evidence. Legacy Error.message is not safe wire data. */
|
|
2
|
+
export type PlanAdmissionIssueCode = 'UNSUPPORTED_PLAN_VERSION' | 'EMPTY_PLAN' | 'DUPLICATE_STEP_ID' | 'UNKNOWN_CAPABILITY' | 'CAPABILITY_VERSION_MISMATCH' | 'REQUIRED_INPUT_MISSING' | 'INPUT_TYPE_MISMATCH' | 'UNSUPPORTED_INPUT_SCHEMA' | 'INVALID_RESULT_REFERENCE' | 'RESULT_REFERENCE_NOT_EARLIER';
|
|
3
|
+
export interface PlanAdmissionIssue {
|
|
4
|
+
readonly code: PlanAdmissionIssueCode;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly stepIndex?: number;
|
|
7
|
+
readonly field?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PlanAdmissionError extends Error {
|
|
10
|
+
readonly code: 'PLAN_ADMISSION_REJECTED';
|
|
11
|
+
readonly issues: readonly PlanAdmissionIssue[];
|
|
12
|
+
}
|
|
13
|
+
export type AdmissionOwner = object;
|
|
14
|
+
/** Local issuance only; current-call certainty requires the direct executePlan boundary. */
|
|
15
|
+
export declare function isPlanAdmissionError(value: unknown): value is PlanAdmissionError;
|
|
16
|
+
export declare function issuePlanAdmissionError(owner: AdmissionOwner, message: string, issues: readonly {
|
|
17
|
+
code: PlanAdmissionIssueCode;
|
|
18
|
+
stepIndex?: number;
|
|
19
|
+
field?: string;
|
|
20
|
+
}[]): PlanAdmissionError;
|
|
21
|
+
/** Prevent a genuine diagnostic from another submission becoming this call's evidence. */
|
|
22
|
+
export declare function containForeignAdmissionError(owner: AdmissionOwner, error: unknown): unknown;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPlanAdmissionError = isPlanAdmissionError;
|
|
4
|
+
exports.issuePlanAdmissionError = issuePlanAdmissionError;
|
|
5
|
+
exports.containForeignAdmissionError = containForeignAdmissionError;
|
|
6
|
+
const messages = {
|
|
7
|
+
UNSUPPORTED_PLAN_VERSION: 'The plan version is not supported.',
|
|
8
|
+
EMPTY_PLAN: 'The plan contains no steps.',
|
|
9
|
+
DUPLICATE_STEP_ID: 'A step identifier is repeated.',
|
|
10
|
+
UNKNOWN_CAPABILITY: 'The requested capability is not registered.',
|
|
11
|
+
CAPABILITY_VERSION_MISMATCH: 'The requested capability version does not match.',
|
|
12
|
+
REQUIRED_INPUT_MISSING: 'A required input field is missing.',
|
|
13
|
+
INPUT_TYPE_MISMATCH: 'An input field has an incompatible type.',
|
|
14
|
+
UNSUPPORTED_INPUT_SCHEMA: 'An input field cannot be validated with the declared schema.',
|
|
15
|
+
INVALID_RESULT_REFERENCE: 'A result reference was not accepted by the admission parser.',
|
|
16
|
+
RESULT_REFERENCE_NOT_EARLIER: 'A result reference must target an earlier declared step.',
|
|
17
|
+
};
|
|
18
|
+
const issued = new WeakMap();
|
|
19
|
+
/** Local issuance only; current-call certainty requires the direct executePlan boundary. */
|
|
20
|
+
function isPlanAdmissionError(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null && issued.has(value);
|
|
22
|
+
}
|
|
23
|
+
function issuePlanAdmissionError(owner, message, issues) {
|
|
24
|
+
const details = Object.freeze(issues.map(issue => Object.freeze({
|
|
25
|
+
code: issue.code,
|
|
26
|
+
message: messages[issue.code],
|
|
27
|
+
...(issue.stepIndex === undefined ? {} : { stepIndex: issue.stepIndex }),
|
|
28
|
+
...(issue.field === undefined ? {} : { field: issue.field }),
|
|
29
|
+
})));
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
Object.defineProperties(error, {
|
|
32
|
+
code: { value: 'PLAN_ADMISSION_REJECTED', writable: false, configurable: false, enumerable: false },
|
|
33
|
+
issues: { value: details, writable: false, configurable: false, enumerable: false },
|
|
34
|
+
});
|
|
35
|
+
issued.set(error, { owner, message });
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
/** Prevent a genuine diagnostic from another submission becoming this call's evidence. */
|
|
39
|
+
function containForeignAdmissionError(owner, error) {
|
|
40
|
+
if (!isPlanAdmissionError(error))
|
|
41
|
+
return error;
|
|
42
|
+
const record = issued.get(error);
|
|
43
|
+
return record.owner === owner ? error : new Error(record.message, { cause: error });
|
|
44
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { PlanAdmissionIssueCode } from './plan-admission-error.js';
|
|
1
2
|
import { ExecutionStep } from '../planner/planner.js';
|
|
2
3
|
export interface PlanValidationError {
|
|
4
|
+
code: PlanAdmissionIssueCode;
|
|
5
|
+
stepIndex?: number;
|
|
3
6
|
stepId: string;
|
|
4
7
|
capability: string;
|
|
5
8
|
field?: string;
|
|
@@ -23,6 +23,7 @@ function validateInput(step, capability, allowReferences) {
|
|
|
23
23
|
stepId: step.id,
|
|
24
24
|
capability: step.capability,
|
|
25
25
|
field,
|
|
26
|
+
code: 'REQUIRED_INPUT_MISSING',
|
|
26
27
|
message: `Required input '${field}' is missing for capability '${step.capability}'`,
|
|
27
28
|
});
|
|
28
29
|
continue;
|
|
@@ -36,6 +37,7 @@ function validateInput(step, capability, allowReferences) {
|
|
|
36
37
|
stepId: step.id,
|
|
37
38
|
capability: step.capability,
|
|
38
39
|
field,
|
|
40
|
+
code: 'UNSUPPORTED_INPUT_SCHEMA',
|
|
39
41
|
message: `Unsupported schema type '${definition.type}' for capability '${step.capability}'`,
|
|
40
42
|
});
|
|
41
43
|
}
|
|
@@ -44,6 +46,7 @@ function validateInput(step, capability, allowReferences) {
|
|
|
44
46
|
stepId: step.id,
|
|
45
47
|
capability: step.capability,
|
|
46
48
|
field,
|
|
49
|
+
code: 'INPUT_TYPE_MISMATCH',
|
|
47
50
|
message: `Input '${field}' must be of type '${definition.type}' for capability '${step.capability}'`,
|
|
48
51
|
});
|
|
49
52
|
}
|
|
@@ -68,6 +71,7 @@ function validateStepInput(step, input) {
|
|
|
68
71
|
errors: [{
|
|
69
72
|
stepId: step.id,
|
|
70
73
|
capability: step.capability,
|
|
74
|
+
code: 'UNKNOWN_CAPABILITY',
|
|
71
75
|
message: `Unknown capability: ${step.capability}`,
|
|
72
76
|
}],
|
|
73
77
|
};
|
|
@@ -78,21 +82,34 @@ function validateStepInput(step, input) {
|
|
|
78
82
|
function validatePlan(steps) {
|
|
79
83
|
const errors = [];
|
|
80
84
|
const seenStepIds = new Set();
|
|
81
|
-
for (const step of steps) {
|
|
85
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
86
|
+
const firstIssue = errors.length;
|
|
87
|
+
if (seenStepIds.has(step.id)) {
|
|
88
|
+
errors.push({
|
|
89
|
+
stepId: step.id,
|
|
90
|
+
capability: step.capability,
|
|
91
|
+
code: 'DUPLICATE_STEP_ID',
|
|
92
|
+
message: `Duplicate step ID: ${step.id}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
82
95
|
const capability = registry_js_1.capabilityRegistry.get(step.capability);
|
|
83
96
|
if (!capability) {
|
|
84
97
|
errors.push({
|
|
85
98
|
stepId: step.id,
|
|
86
99
|
capability: step.capability,
|
|
100
|
+
code: 'UNKNOWN_CAPABILITY',
|
|
87
101
|
message: `Unknown capability: ${step.capability}`,
|
|
88
102
|
});
|
|
103
|
+
for (let i = firstIssue; i < errors.length; i++)
|
|
104
|
+
errors[i].stepIndex = stepIndex;
|
|
89
105
|
seenStepIds.add(step.id);
|
|
90
106
|
continue;
|
|
91
107
|
}
|
|
92
|
-
if (step.capabilityVersion && step.capabilityVersion !== capability.version) {
|
|
108
|
+
if (step.capabilityVersion !== undefined && step.capabilityVersion !== capability.version) {
|
|
93
109
|
errors.push({
|
|
94
110
|
stepId: step.id,
|
|
95
111
|
capability: step.capability,
|
|
112
|
+
code: 'CAPABILITY_VERSION_MISMATCH',
|
|
96
113
|
message: `Capability version mismatch for '${step.capability}': requested ${step.capabilityVersion}, registered ${capability.version}`,
|
|
97
114
|
});
|
|
98
115
|
}
|
|
@@ -104,6 +121,7 @@ function validatePlan(steps) {
|
|
|
104
121
|
errors.push({
|
|
105
122
|
stepId: step.id,
|
|
106
123
|
capability: step.capability,
|
|
124
|
+
code: 'RESULT_REFERENCE_NOT_EARLIER',
|
|
107
125
|
message: `Result reference must target an earlier step: ${reference}`,
|
|
108
126
|
});
|
|
109
127
|
}
|
|
@@ -112,10 +130,13 @@ function validatePlan(steps) {
|
|
|
112
130
|
errors.push({
|
|
113
131
|
stepId: step.id,
|
|
114
132
|
capability: step.capability,
|
|
133
|
+
code: 'INVALID_RESULT_REFERENCE',
|
|
115
134
|
message: error instanceof Error ? error.message : 'Invalid result reference',
|
|
116
135
|
});
|
|
117
136
|
}
|
|
118
137
|
}
|
|
138
|
+
for (let i = firstIssue; i < errors.length; i++)
|
|
139
|
+
errors[i].stepIndex = stepIndex;
|
|
119
140
|
seenStepIds.add(step.id);
|
|
120
141
|
}
|
|
121
142
|
return { valid: errors.length === 0, errors };
|
|
@@ -9,8 +9,15 @@ function isResultReference(value) {
|
|
|
9
9
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const keys = Reflect.ownKeys(value);
|
|
13
|
+
if (keys.length !== 1 || keys[0] !== '$ref') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, '$ref');
|
|
17
|
+
return descriptor !== undefined
|
|
18
|
+
&& descriptor.enumerable === true
|
|
19
|
+
&& Object.hasOwn(descriptor, 'value')
|
|
20
|
+
&& typeof descriptor.value === 'string';
|
|
14
21
|
}
|
|
15
22
|
function parseResultReference(reference) {
|
|
16
23
|
if (!reference.startsWith(REFERENCE_PREFIX)) {
|
|
@@ -39,7 +46,7 @@ function resolveResultReferences(value, completedSteps) {
|
|
|
39
46
|
}
|
|
40
47
|
let resolved = step.result;
|
|
41
48
|
for (const segment of path) {
|
|
42
|
-
if (!resolved || typeof resolved !== 'object' || !(segment
|
|
49
|
+
if (!resolved || typeof resolved !== 'object' || !Object.hasOwn(resolved, segment)) {
|
|
43
50
|
throw new Error(`Result reference path not found: ${value.$ref}`);
|
|
44
51
|
}
|
|
45
52
|
resolved = resolved[segment];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AdmissionOwner } from '../execution/plan-admission-error.js';
|
|
1
2
|
import { ExecutionAuthorizer } from '../permissions/execution-authorizer.js';
|
|
2
3
|
import { ExecutionPlan } from '../planner/planner.js';
|
|
3
4
|
import { ExecutionCaller } from '../execution/execution-context.js';
|
|
@@ -5,12 +6,12 @@ import { Job, JobOutcome } from './job.js';
|
|
|
5
6
|
import { JobStatus } from './job-status.js';
|
|
6
7
|
import { JobListFilter } from './job-store.js';
|
|
7
8
|
declare class JobManager {
|
|
8
|
-
executePlan(plan: ExecutionPlan, caller?: ExecutionCaller, authorizer?: ExecutionAuthorizer): Promise<Job>;
|
|
9
|
+
executePlan(plan: ExecutionPlan, caller?: ExecutionCaller, authorizer?: ExecutionAuthorizer, admissionOwner?: AdmissionOwner, enabledVersions?: ReadonlySet<string>): Promise<Job>;
|
|
9
10
|
create(goal: string, planner?: string): Promise<Job>;
|
|
10
11
|
get(id: string): Promise<Job | undefined>;
|
|
11
12
|
list(filter?: JobListFilter): Promise<Job[]>;
|
|
12
13
|
setStatus(id: string, status: JobStatus): Promise<Job>;
|
|
13
|
-
execute(id: string, caller?: ExecutionCaller, authorizer?: ExecutionAuthorizer): Promise<Job>;
|
|
14
|
+
execute(id: string, caller?: ExecutionCaller, authorizer?: ExecutionAuthorizer, semanticVersion?: '1.0' | '2.0'): Promise<Job>;
|
|
14
15
|
review(id: string, outcome: JobOutcome, notes?: string): Promise<Job>;
|
|
15
16
|
private addEvent;
|
|
16
17
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jobManager = void 0;
|
|
4
|
+
const plan_admission_error_js_1 = require("../execution/plan-admission-error.js");
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const registry_js_1 = require("../registry/registry.js");
|
|
6
7
|
const execution_authorizer_js_1 = require("../permissions/execution-authorizer.js");
|
|
7
8
|
const plan_validator_js_1 = require("../execution/plan-validator.js");
|
|
8
9
|
const result_reference_js_1 = require("../execution/result-reference.js");
|
|
10
|
+
const governed_value_js_1 = require("../execution/governed-value.js");
|
|
9
11
|
const memory_event_bus_js_1 = require("../events/memory-event-bus.js");
|
|
10
12
|
const job_store_js_1 = require("./job-store.js");
|
|
11
13
|
const console_execution_logger_js_1 = require("../execution/console-execution-logger.js");
|
|
@@ -13,21 +15,43 @@ const console_log_sink_js_1 = require("../logging/console-log-sink.js");
|
|
|
13
15
|
const composite_log_sink_js_1 = require("../logging/composite-log-sink.js");
|
|
14
16
|
const sqlite_log_sink_js_1 = require("../logging/sqlite-log-sink.js");
|
|
15
17
|
class JobManager {
|
|
16
|
-
async executePlan(plan, caller, authorizer = execution_authorizer_js_1.defaultExecutionAuthorizer) {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
async executePlan(plan, caller, authorizer = execution_authorizer_js_1.defaultExecutionAuthorizer, admissionOwner = {}, enabledVersions = new Set(['1.0'])) {
|
|
19
|
+
const version = plan.version;
|
|
20
|
+
if ((version !== '1.0' && version !== '2.0') || !enabledVersions.has(version)) {
|
|
21
|
+
throw (0, plan_admission_error_js_1.issuePlanAdmissionError)(admissionOwner, "Execution plan version is not supported; expected '1.0'.", [{ code: 'UNSUPPORTED_PLAN_VERSION' }]);
|
|
19
22
|
}
|
|
20
|
-
|
|
23
|
+
// Own the structural envelope before admission; nested input remains shared.
|
|
24
|
+
const capturedGoal = plan.goal;
|
|
25
|
+
const idempotencyKey = plan.idempotencyKey;
|
|
26
|
+
const submittedSteps = plan.steps;
|
|
27
|
+
const steps = new Array(submittedSteps.length);
|
|
28
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
29
|
+
if (!(index in submittedSteps))
|
|
30
|
+
continue;
|
|
31
|
+
const step = submittedSteps[index];
|
|
32
|
+
steps[index] = {
|
|
33
|
+
id: step.id,
|
|
34
|
+
capability: step.capability,
|
|
35
|
+
capabilityVersion: step.capabilityVersion,
|
|
36
|
+
input: step.input,
|
|
37
|
+
reason: step.reason,
|
|
38
|
+
idempotencyKey: step.idempotencyKey,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (!steps.length) {
|
|
42
|
+
throw (0, plan_admission_error_js_1.issuePlanAdmissionError)(admissionOwner, 'Execution plan contains no steps', [{ code: 'EMPTY_PLAN' }]);
|
|
43
|
+
}
|
|
44
|
+
const validation = (0, plan_validator_js_1.validatePlan)(steps);
|
|
21
45
|
if (!validation.valid) {
|
|
22
|
-
throw
|
|
46
|
+
throw (0, plan_admission_error_js_1.issuePlanAdmissionError)(admissionOwner, `Execution plan failed validation: ${validation.errors
|
|
23
47
|
.map((error) => error.message)
|
|
24
|
-
.join('; ')}
|
|
48
|
+
.join('; ')}`, validation.errors);
|
|
25
49
|
}
|
|
26
|
-
const goal =
|
|
50
|
+
const goal = capturedGoal?.trim() ||
|
|
27
51
|
'External execution plan';
|
|
28
52
|
const job = await this.create(goal);
|
|
29
|
-
job.idempotencyKey =
|
|
30
|
-
job.steps =
|
|
53
|
+
job.idempotencyKey = idempotencyKey;
|
|
54
|
+
job.steps = steps.map((step) => ({
|
|
31
55
|
...step,
|
|
32
56
|
status: 'pending',
|
|
33
57
|
createdAt: new Date().toISOString(),
|
|
@@ -35,7 +59,7 @@ class JobManager {
|
|
|
35
59
|
job.updatedAt =
|
|
36
60
|
new Date().toISOString();
|
|
37
61
|
await job_store_js_1.jobStore.update(job);
|
|
38
|
-
return this.execute(job.id, caller, authorizer);
|
|
62
|
+
return this.execute(job.id, caller, authorizer, version);
|
|
39
63
|
}
|
|
40
64
|
async create(goal, planner) {
|
|
41
65
|
if (!goal?.trim()) {
|
|
@@ -74,7 +98,7 @@ class JobManager {
|
|
|
74
98
|
await job_store_js_1.jobStore.update(job);
|
|
75
99
|
return job;
|
|
76
100
|
}
|
|
77
|
-
async execute(id, caller, authorizer = execution_authorizer_js_1.defaultExecutionAuthorizer) {
|
|
101
|
+
async execute(id, caller, authorizer = execution_authorizer_js_1.defaultExecutionAuthorizer, semanticVersion = '1.0') {
|
|
78
102
|
const job = await job_store_js_1.jobStore.get(id);
|
|
79
103
|
if (!job) {
|
|
80
104
|
throw new Error(`Job not found: ${id}`);
|
|
@@ -94,26 +118,48 @@ class JobManager {
|
|
|
94
118
|
}
|
|
95
119
|
try {
|
|
96
120
|
const completedSteps = job.steps.slice(0, job.steps.indexOf(step));
|
|
121
|
+
// ADR-0011 begins after the existing resolver. Reference traversal
|
|
122
|
+
// may retain its legacy getter/Proxy behavior; capture governs only
|
|
123
|
+
// the representation returned by that resolver.
|
|
97
124
|
const resolvedInput = (0, result_reference_js_1.resolveResultReferences)(step.input, completedSteps);
|
|
98
|
-
const
|
|
125
|
+
const governedInput = semanticVersion === '2.0'
|
|
126
|
+
? (0, governed_value_js_1.captureGovernedValue)(resolvedInput)
|
|
127
|
+
: resolvedInput;
|
|
128
|
+
const inputValidation = (0, plan_validator_js_1.validateStepInput)(step, governedInput);
|
|
99
129
|
if (!inputValidation.valid) {
|
|
100
130
|
throw new Error(inputValidation.errors
|
|
101
131
|
.map((validationError) => validationError.message)
|
|
102
132
|
.join('; '));
|
|
103
133
|
}
|
|
104
|
-
const authorization =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
const authorization = semanticVersion === '2.0'
|
|
135
|
+
? await authorizer.authorize(createGovernedAuthorizationContext(job.id, step.id, capability, (0, governed_value_js_1.copyGovernedValue)(governedInput, true), caller))
|
|
136
|
+
: await authorizer.authorize({
|
|
137
|
+
jobId: job.id,
|
|
138
|
+
stepId: step.id,
|
|
139
|
+
capability: {
|
|
140
|
+
name: capability.name,
|
|
141
|
+
version: capability.version,
|
|
142
|
+
risk: capability.risk,
|
|
143
|
+
},
|
|
144
|
+
input: resolvedInput,
|
|
145
|
+
caller,
|
|
146
|
+
});
|
|
147
|
+
if (typeof authorization !== 'object' ||
|
|
148
|
+
authorization === null ||
|
|
149
|
+
Array.isArray(authorization) ||
|
|
150
|
+
!Object.hasOwn(authorization, 'decision')) {
|
|
151
|
+
throw new Error('Invalid authorization decision');
|
|
152
|
+
}
|
|
153
|
+
const decision = authorization.decision;
|
|
154
|
+
if (decision !== 'allow' && decision !== 'deny') {
|
|
155
|
+
throw new Error('Invalid authorization decision');
|
|
156
|
+
}
|
|
157
|
+
if (decision === 'deny') {
|
|
158
|
+
const reason = authorization.reason;
|
|
159
|
+
if (reason !== undefined && typeof reason !== 'string') {
|
|
160
|
+
throw new Error('Invalid authorization decision');
|
|
161
|
+
}
|
|
162
|
+
const message = reason ??
|
|
117
163
|
`Capability not permitted: ${step.capability}`;
|
|
118
164
|
step.status = 'failed';
|
|
119
165
|
step.error = message;
|
|
@@ -122,10 +168,15 @@ class JobManager {
|
|
|
122
168
|
stepId: step.id,
|
|
123
169
|
capability: capability.name,
|
|
124
170
|
risk: capability.risk,
|
|
125
|
-
reason
|
|
171
|
+
reason,
|
|
126
172
|
});
|
|
127
173
|
throw new AuthorizationDeniedError(message);
|
|
128
174
|
}
|
|
175
|
+
// ADR-0011 requires the detached v2 entry value to exist before the
|
|
176
|
+
// running/start transition. V1 retains its historical shared input.
|
|
177
|
+
const capabilityInput = semanticVersion === '2.0'
|
|
178
|
+
? (0, governed_value_js_1.copyGovernedValue)(governedInput)
|
|
179
|
+
: resolvedInput;
|
|
129
180
|
step.status = 'running';
|
|
130
181
|
step.startedAt = new Date().toISOString();
|
|
131
182
|
this.addEvent(job, 'capability.started', {
|
|
@@ -136,7 +187,7 @@ class JobManager {
|
|
|
136
187
|
new console_log_sink_js_1.ConsoleLogSink(),
|
|
137
188
|
new sqlite_log_sink_js_1.SQLiteLogSink(),
|
|
138
189
|
]));
|
|
139
|
-
const result = await capability.execute(
|
|
190
|
+
const result = await capability.execute(capabilityInput, {
|
|
140
191
|
jobId: job.id,
|
|
141
192
|
stepId: step.id,
|
|
142
193
|
logger,
|
|
@@ -234,6 +285,25 @@ class JobManager {
|
|
|
234
285
|
return event;
|
|
235
286
|
}
|
|
236
287
|
}
|
|
288
|
+
function createGovernedAuthorizationContext(jobId, stepId, capability, input, caller) {
|
|
289
|
+
const context = {
|
|
290
|
+
jobId,
|
|
291
|
+
stepId,
|
|
292
|
+
capability: {
|
|
293
|
+
name: capability.name,
|
|
294
|
+
version: capability.version,
|
|
295
|
+
risk: capability.risk,
|
|
296
|
+
},
|
|
297
|
+
caller,
|
|
298
|
+
};
|
|
299
|
+
Object.defineProperty(context, 'input', {
|
|
300
|
+
value: input,
|
|
301
|
+
enumerable: true,
|
|
302
|
+
configurable: false,
|
|
303
|
+
writable: false,
|
|
304
|
+
});
|
|
305
|
+
return context;
|
|
306
|
+
}
|
|
237
307
|
class AuthorizationDeniedError extends Error {
|
|
238
308
|
}
|
|
239
309
|
exports.jobManager = new JobManager();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CapabilityDescriptor } from './registry/capability.js';
|
|
1
2
|
import { Job } from './jobs/job.js';
|
|
2
3
|
import { JobListFilter } from './jobs/job-store.js';
|
|
3
4
|
import { ExecutionCaller } from './execution/execution-context.js';
|
|
@@ -14,22 +15,19 @@ export interface ExecutePlanOptions {
|
|
|
14
15
|
}
|
|
15
16
|
export interface OperatorRuntimeOptions {
|
|
16
17
|
readonly authorizer?: ExecutionAuthorizer;
|
|
18
|
+
readonly planVersions?: readonly string[];
|
|
17
19
|
}
|
|
18
20
|
export declare class OperatorRuntime {
|
|
19
21
|
private readonly authorizer;
|
|
22
|
+
private readonly planVersions;
|
|
20
23
|
constructor(options?: OperatorRuntimeOptions);
|
|
21
24
|
use(module: CapabilityModule): void;
|
|
22
25
|
executePlan(plan: ExecutionPlan, options?: ExecutePlanOptions): Promise<Job>;
|
|
23
26
|
run(goal: string, options?: RunJobOptions): Promise<Job>;
|
|
24
27
|
getJob(id: string): Promise<Job | undefined>;
|
|
25
28
|
listJobs(filter?: JobListFilter): Promise<Job[]>;
|
|
26
|
-
listCapabilities():
|
|
27
|
-
|
|
28
|
-
version: string;
|
|
29
|
-
description: string;
|
|
30
|
-
risk: import("./registry/capability.js").CapabilityRisk;
|
|
31
|
-
inputSchema: Record<string, import("./registry/capability.js").CapabilityInputField>;
|
|
32
|
-
}[];
|
|
29
|
+
listCapabilities(): CapabilityDescriptor[];
|
|
30
|
+
describeCapability(name: string, version?: string): CapabilityDescriptor | undefined;
|
|
33
31
|
listPlanners(): {
|
|
34
32
|
name: string;
|
|
35
33
|
}[];
|