@veil-runtime/core 0.2.0 → 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 +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -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 +15 -1
- package/dist/runtime/execution/result-reference.js +9 -2
- package/dist/runtime/jobs/job-manager.d.ts +3 -2
- package/dist/runtime/jobs/job-manager.js +57 -19
- package/dist/runtime/operator-runtime.d.ts +2 -0
- package/dist/runtime/operator-runtime.js +17 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Requires Node.js 24+.
|
|
|
21
21
|
|
|
22
22
|
npm install @veil-runtime/core
|
|
23
23
|
|
|
24
|
-
This installs the published package. To verify v0.
|
|
24
|
+
This installs the published package. To verify the v0.3.0 candidate before publication, see [candidate installation](docs/getting-started/installation.md).
|
|
25
25
|
|
|
26
26
|
## Smallest working example
|
|
27
27
|
|
|
@@ -47,7 +47,7 @@ The default authorizer permits reads and denies write/destructive capabilities.
|
|
|
47
47
|
|
|
48
48
|
## Status and roadmap
|
|
49
49
|
|
|
50
|
-
Release candidate: **v0.
|
|
50
|
+
Release candidate: **v0.3.0**. See the [v0.3.0 release notes](docs/getting-started/v0.3.0.md). The latest published package remains **v0.2.0** until publication. ExecutionPlan V1 remains the default; opt-in [ExecutionPlan V2](docs/reference/execution-plan-v2.md) adds governed receiving-value ownership. Both are linear. DAGs, parallel execution, conditionals, cancellation, and retry policies are roadmap work, not current behavior.
|
|
51
51
|
|
|
52
52
|
## Contributing
|
|
53
53
|
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,5 @@ export type { JobEvent } from './runtime/jobs/job-event.js';
|
|
|
14
14
|
export type { RuntimeEvent } from './runtime/events/runtime-event.js';
|
|
15
15
|
export { McpAdapter } from './integrations/mcp/inbound/mcp-adapter.js';
|
|
16
16
|
export * from './sdk/index.js';
|
|
17
|
+
export { isPlanAdmissionError } from './runtime/execution/plan-admission-error.js';
|
|
18
|
+
export type { PlanAdmissionError, PlanAdmissionIssue, PlanAdmissionIssueCode } from './runtime/execution/plan-admission-error.js';
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.McpAdapter = exports.operatorRuntime = exports.OperatorRuntime = void 0;
|
|
17
|
+
exports.isPlanAdmissionError = exports.McpAdapter = exports.operatorRuntime = exports.OperatorRuntime = void 0;
|
|
18
18
|
var operator_runtime_js_1 = require("./runtime/operator-runtime.js");
|
|
19
19
|
Object.defineProperty(exports, "OperatorRuntime", { enumerable: true, get: function () { return operator_runtime_js_1.OperatorRuntime; } });
|
|
20
20
|
Object.defineProperty(exports, "operatorRuntime", { enumerable: true, get: function () { return operator_runtime_js_1.operatorRuntime; } });
|
|
21
21
|
var mcp_adapter_js_1 = require("./integrations/mcp/inbound/mcp-adapter.js");
|
|
22
22
|
Object.defineProperty(exports, "McpAdapter", { enumerable: true, get: function () { return mcp_adapter_js_1.McpAdapter; } });
|
|
23
23
|
__exportStar(require("./sdk/index.js"), exports);
|
|
24
|
+
var plan_admission_error_js_1 = require("./runtime/execution/plan-admission-error.js");
|
|
25
|
+
Object.defineProperty(exports, "isPlanAdmissionError", { enumerable: true, get: function () { return plan_admission_error_js_1.isPlanAdmissionError; } });
|
|
@@ -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,11 +82,13 @@ 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;
|
|
82
87
|
if (seenStepIds.has(step.id)) {
|
|
83
88
|
errors.push({
|
|
84
89
|
stepId: step.id,
|
|
85
90
|
capability: step.capability,
|
|
91
|
+
code: 'DUPLICATE_STEP_ID',
|
|
86
92
|
message: `Duplicate step ID: ${step.id}`,
|
|
87
93
|
});
|
|
88
94
|
}
|
|
@@ -91,8 +97,11 @@ function validatePlan(steps) {
|
|
|
91
97
|
errors.push({
|
|
92
98
|
stepId: step.id,
|
|
93
99
|
capability: step.capability,
|
|
100
|
+
code: 'UNKNOWN_CAPABILITY',
|
|
94
101
|
message: `Unknown capability: ${step.capability}`,
|
|
95
102
|
});
|
|
103
|
+
for (let i = firstIssue; i < errors.length; i++)
|
|
104
|
+
errors[i].stepIndex = stepIndex;
|
|
96
105
|
seenStepIds.add(step.id);
|
|
97
106
|
continue;
|
|
98
107
|
}
|
|
@@ -100,6 +109,7 @@ function validatePlan(steps) {
|
|
|
100
109
|
errors.push({
|
|
101
110
|
stepId: step.id,
|
|
102
111
|
capability: step.capability,
|
|
112
|
+
code: 'CAPABILITY_VERSION_MISMATCH',
|
|
103
113
|
message: `Capability version mismatch for '${step.capability}': requested ${step.capabilityVersion}, registered ${capability.version}`,
|
|
104
114
|
});
|
|
105
115
|
}
|
|
@@ -111,6 +121,7 @@ function validatePlan(steps) {
|
|
|
111
121
|
errors.push({
|
|
112
122
|
stepId: step.id,
|
|
113
123
|
capability: step.capability,
|
|
124
|
+
code: 'RESULT_REFERENCE_NOT_EARLIER',
|
|
114
125
|
message: `Result reference must target an earlier step: ${reference}`,
|
|
115
126
|
});
|
|
116
127
|
}
|
|
@@ -119,10 +130,13 @@ function validatePlan(steps) {
|
|
|
119
130
|
errors.push({
|
|
120
131
|
stepId: step.id,
|
|
121
132
|
capability: step.capability,
|
|
133
|
+
code: 'INVALID_RESULT_REFERENCE',
|
|
122
134
|
message: error instanceof Error ? error.message : 'Invalid result reference',
|
|
123
135
|
});
|
|
124
136
|
}
|
|
125
137
|
}
|
|
138
|
+
for (let i = firstIssue; i < errors.length; i++)
|
|
139
|
+
errors[i].stepIndex = stepIndex;
|
|
126
140
|
seenStepIds.add(step.id);
|
|
127
141
|
}
|
|
128
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)) {
|
|
@@ -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,7 +15,11 @@ 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) {
|
|
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' }]);
|
|
22
|
+
}
|
|
17
23
|
// Own the structural envelope before admission; nested input remains shared.
|
|
18
24
|
const capturedGoal = plan.goal;
|
|
19
25
|
const idempotencyKey = plan.idempotencyKey;
|
|
@@ -33,13 +39,13 @@ class JobManager {
|
|
|
33
39
|
};
|
|
34
40
|
}
|
|
35
41
|
if (!steps.length) {
|
|
36
|
-
throw
|
|
42
|
+
throw (0, plan_admission_error_js_1.issuePlanAdmissionError)(admissionOwner, 'Execution plan contains no steps', [{ code: 'EMPTY_PLAN' }]);
|
|
37
43
|
}
|
|
38
44
|
const validation = (0, plan_validator_js_1.validatePlan)(steps);
|
|
39
45
|
if (!validation.valid) {
|
|
40
|
-
throw
|
|
46
|
+
throw (0, plan_admission_error_js_1.issuePlanAdmissionError)(admissionOwner, `Execution plan failed validation: ${validation.errors
|
|
41
47
|
.map((error) => error.message)
|
|
42
|
-
.join('; ')}
|
|
48
|
+
.join('; ')}`, validation.errors);
|
|
43
49
|
}
|
|
44
50
|
const goal = capturedGoal?.trim() ||
|
|
45
51
|
'External execution plan';
|
|
@@ -53,7 +59,7 @@ class JobManager {
|
|
|
53
59
|
job.updatedAt =
|
|
54
60
|
new Date().toISOString();
|
|
55
61
|
await job_store_js_1.jobStore.update(job);
|
|
56
|
-
return this.execute(job.id, caller, authorizer);
|
|
62
|
+
return this.execute(job.id, caller, authorizer, version);
|
|
57
63
|
}
|
|
58
64
|
async create(goal, planner) {
|
|
59
65
|
if (!goal?.trim()) {
|
|
@@ -92,7 +98,7 @@ class JobManager {
|
|
|
92
98
|
await job_store_js_1.jobStore.update(job);
|
|
93
99
|
return job;
|
|
94
100
|
}
|
|
95
|
-
async execute(id, caller, authorizer = execution_authorizer_js_1.defaultExecutionAuthorizer) {
|
|
101
|
+
async execute(id, caller, authorizer = execution_authorizer_js_1.defaultExecutionAuthorizer, semanticVersion = '1.0') {
|
|
96
102
|
const job = await job_store_js_1.jobStore.get(id);
|
|
97
103
|
if (!job) {
|
|
98
104
|
throw new Error(`Job not found: ${id}`);
|
|
@@ -112,24 +118,32 @@ class JobManager {
|
|
|
112
118
|
}
|
|
113
119
|
try {
|
|
114
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.
|
|
115
124
|
const resolvedInput = (0, result_reference_js_1.resolveResultReferences)(step.input, completedSteps);
|
|
116
|
-
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);
|
|
117
129
|
if (!inputValidation.valid) {
|
|
118
130
|
throw new Error(inputValidation.errors
|
|
119
131
|
.map((validationError) => validationError.message)
|
|
120
132
|
.join('; '));
|
|
121
133
|
}
|
|
122
|
-
const authorization =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
});
|
|
133
147
|
if (typeof authorization !== 'object' ||
|
|
134
148
|
authorization === null ||
|
|
135
149
|
Array.isArray(authorization) ||
|
|
@@ -158,6 +172,11 @@ class JobManager {
|
|
|
158
172
|
});
|
|
159
173
|
throw new AuthorizationDeniedError(message);
|
|
160
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;
|
|
161
180
|
step.status = 'running';
|
|
162
181
|
step.startedAt = new Date().toISOString();
|
|
163
182
|
this.addEvent(job, 'capability.started', {
|
|
@@ -168,7 +187,7 @@ class JobManager {
|
|
|
168
187
|
new console_log_sink_js_1.ConsoleLogSink(),
|
|
169
188
|
new sqlite_log_sink_js_1.SQLiteLogSink(),
|
|
170
189
|
]));
|
|
171
|
-
const result = await capability.execute(
|
|
190
|
+
const result = await capability.execute(capabilityInput, {
|
|
172
191
|
jobId: job.id,
|
|
173
192
|
stepId: step.id,
|
|
174
193
|
logger,
|
|
@@ -266,6 +285,25 @@ class JobManager {
|
|
|
266
285
|
return event;
|
|
267
286
|
}
|
|
268
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
|
+
}
|
|
269
307
|
class AuthorizationDeniedError extends Error {
|
|
270
308
|
}
|
|
271
309
|
exports.jobManager = new JobManager();
|
|
@@ -15,9 +15,11 @@ export interface ExecutePlanOptions {
|
|
|
15
15
|
}
|
|
16
16
|
export interface OperatorRuntimeOptions {
|
|
17
17
|
readonly authorizer?: ExecutionAuthorizer;
|
|
18
|
+
readonly planVersions?: readonly string[];
|
|
18
19
|
}
|
|
19
20
|
export declare class OperatorRuntime {
|
|
20
21
|
private readonly authorizer;
|
|
22
|
+
private readonly planVersions;
|
|
21
23
|
constructor(options?: OperatorRuntimeOptions);
|
|
22
24
|
use(module: CapabilityModule): void;
|
|
23
25
|
executePlan(plan: ExecutionPlan, options?: ExecutePlanOptions): Promise<Job>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.operatorRuntime = exports.OperatorRuntime = void 0;
|
|
4
|
+
const plan_admission_error_js_1 = require("./execution/plan-admission-error.js");
|
|
4
5
|
const job_manager_js_1 = require("./jobs/job-manager.js");
|
|
5
6
|
const job_memory_js_1 = require("./jobs/job-memory.js");
|
|
6
7
|
const execution_authorizer_js_1 = require("./permissions/execution-authorizer.js");
|
|
@@ -23,10 +24,19 @@ function immutableCaller(caller) {
|
|
|
23
24
|
}
|
|
24
25
|
class OperatorRuntime {
|
|
25
26
|
authorizer;
|
|
27
|
+
planVersions;
|
|
26
28
|
constructor(options = {}) {
|
|
27
29
|
this.authorizer =
|
|
28
30
|
options.authorizer ??
|
|
29
31
|
execution_authorizer_js_1.defaultExecutionAuthorizer;
|
|
32
|
+
const versions = options.planVersions ?? ['1.0'];
|
|
33
|
+
if (!Array.isArray(versions) ||
|
|
34
|
+
versions.length === 0 ||
|
|
35
|
+
versions.some((version) => version !== '1.0' && version !== '2.0') ||
|
|
36
|
+
new Set(versions).size !== versions.length) {
|
|
37
|
+
throw new Error('planVersions must contain unique supported semantic versions');
|
|
38
|
+
}
|
|
39
|
+
this.planVersions = new Set(versions);
|
|
30
40
|
}
|
|
31
41
|
use(module) {
|
|
32
42
|
const declared = new Set(module.manifest.capabilities);
|
|
@@ -38,7 +48,13 @@ class OperatorRuntime {
|
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
async executePlan(plan, options = {}) {
|
|
41
|
-
|
|
51
|
+
const admissionOwner = {};
|
|
52
|
+
try {
|
|
53
|
+
return await job_manager_js_1.jobManager.executePlan(plan, immutableCaller(options.caller), this.authorizer, admissionOwner, this.planVersions);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw (0, plan_admission_error_js_1.containForeignAdmissionError)(admissionOwner, error);
|
|
57
|
+
}
|
|
42
58
|
}
|
|
43
59
|
async run(goal, options = {}) {
|
|
44
60
|
const router = planner_router_registry_js_1.plannerRouterRegistry.getDefault();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veil-runtime/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A governed, capability-driven execution runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"better-sqlite3": "^13.0.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@babel/parser": "8.0.
|
|
66
|
+
"@babel/parser": "8.0.6",
|
|
67
67
|
"@types/better-sqlite3": "^9.6.0",
|
|
68
68
|
"@types/node": "^26.2.0",
|
|
69
69
|
"fastify": "^5.11.3",
|