@victframework/runtime 0.1.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.
Files changed (115) hide show
  1. package/dist/agent-governance.d.ts +285 -0
  2. package/dist/agent-governance.js +438 -0
  3. package/dist/agent-governance.js.map +1 -0
  4. package/dist/agent-registry.d.ts +103 -0
  5. package/dist/agent-registry.js +758 -0
  6. package/dist/agent-registry.js.map +1 -0
  7. package/dist/agent-types.d.ts +433 -0
  8. package/dist/agent-types.js +480 -0
  9. package/dist/agent-types.js.map +1 -0
  10. package/dist/authority.d.ts +83 -0
  11. package/dist/authority.js +170 -0
  12. package/dist/authority.js.map +1 -0
  13. package/dist/boundary-conformance.d.ts +51 -0
  14. package/dist/boundary-conformance.js +364 -0
  15. package/dist/boundary-conformance.js.map +1 -0
  16. package/dist/control-conformance.d.ts +68 -0
  17. package/dist/control-conformance.js +1131 -0
  18. package/dist/control-conformance.js.map +1 -0
  19. package/dist/control-in-memory.d.ts +257 -0
  20. package/dist/control-in-memory.js +886 -0
  21. package/dist/control-in-memory.js.map +1 -0
  22. package/dist/control-types.d.ts +1017 -0
  23. package/dist/control-types.js +779 -0
  24. package/dist/control-types.js.map +1 -0
  25. package/dist/durable-run.d.ts +73 -0
  26. package/dist/durable-run.js +262 -0
  27. package/dist/durable-run.js.map +1 -0
  28. package/dist/effect-policy.d.ts +22 -0
  29. package/dist/effect-policy.js +45 -0
  30. package/dist/effect-policy.js.map +1 -0
  31. package/dist/errors.d.ts +36 -0
  32. package/dist/errors.js +71 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/in-memory-stores.d.ts +16 -0
  35. package/dist/in-memory-stores.js +522 -0
  36. package/dist/in-memory-stores.js.map +1 -0
  37. package/dist/index.d.ts +41 -0
  38. package/dist/index.js +26 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/operator-config.d.ts +141 -0
  41. package/dist/operator-config.js +289 -0
  42. package/dist/operator-config.js.map +1 -0
  43. package/dist/orchestration-activation.d.ts +116 -0
  44. package/dist/orchestration-activation.js +223 -0
  45. package/dist/orchestration-activation.js.map +1 -0
  46. package/dist/orchestration-canary-conformance.d.ts +10 -0
  47. package/dist/orchestration-canary-conformance.js +726 -0
  48. package/dist/orchestration-canary-conformance.js.map +1 -0
  49. package/dist/orchestration-commands.d.ts +47 -0
  50. package/dist/orchestration-commands.js +538 -0
  51. package/dist/orchestration-commands.js.map +1 -0
  52. package/dist/orchestration-conformance.d.ts +59 -0
  53. package/dist/orchestration-conformance.js +594 -0
  54. package/dist/orchestration-conformance.js.map +1 -0
  55. package/dist/orchestration-driver-types.d.ts +163 -0
  56. package/dist/orchestration-driver-types.js +21 -0
  57. package/dist/orchestration-driver-types.js.map +1 -0
  58. package/dist/orchestration-driver.d.ts +48 -0
  59. package/dist/orchestration-driver.js +1030 -0
  60. package/dist/orchestration-driver.js.map +1 -0
  61. package/dist/orchestration-in-memory.d.ts +18 -0
  62. package/dist/orchestration-in-memory.js +1313 -0
  63. package/dist/orchestration-in-memory.js.map +1 -0
  64. package/dist/orchestration-join-conformance.d.ts +20 -0
  65. package/dist/orchestration-join-conformance.js +542 -0
  66. package/dist/orchestration-join-conformance.js.map +1 -0
  67. package/dist/orchestration-plan.d.ts +52 -0
  68. package/dist/orchestration-plan.js +265 -0
  69. package/dist/orchestration-plan.js.map +1 -0
  70. package/dist/orchestration-race-conformance.d.ts +35 -0
  71. package/dist/orchestration-race-conformance.js +1265 -0
  72. package/dist/orchestration-race-conformance.js.map +1 -0
  73. package/dist/orchestration-remediation-conformance.d.ts +54 -0
  74. package/dist/orchestration-remediation-conformance.js +632 -0
  75. package/dist/orchestration-remediation-conformance.js.map +1 -0
  76. package/dist/orchestration-store-types.d.ts +568 -0
  77. package/dist/orchestration-store-types.js +2 -0
  78. package/dist/orchestration-store-types.js.map +1 -0
  79. package/dist/pack-conformance.d.ts +59 -0
  80. package/dist/pack-conformance.js +222 -0
  81. package/dist/pack-conformance.js.map +1 -0
  82. package/dist/pack-install.d.ts +44 -0
  83. package/dist/pack-install.js +126 -0
  84. package/dist/pack-install.js.map +1 -0
  85. package/dist/registry.d.ts +107 -0
  86. package/dist/registry.js +481 -0
  87. package/dist/registry.js.map +1 -0
  88. package/dist/runtime.d.ts +158 -0
  89. package/dist/runtime.js +1341 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/dist/serialization.d.ts +26 -0
  92. package/dist/serialization.js +162 -0
  93. package/dist/serialization.js.map +1 -0
  94. package/dist/store-conformance.d.ts +45 -0
  95. package/dist/store-conformance.js +876 -0
  96. package/dist/store-conformance.js.map +1 -0
  97. package/dist/store-errors.d.ts +49 -0
  98. package/dist/store-errors.js +48 -0
  99. package/dist/store-errors.js.map +1 -0
  100. package/dist/store-types.d.ts +331 -0
  101. package/dist/store-types.js +14 -0
  102. package/dist/store-types.js.map +1 -0
  103. package/dist/store-validation.d.ts +63 -0
  104. package/dist/store-validation.js +164 -0
  105. package/dist/store-validation.js.map +1 -0
  106. package/dist/stream-hub.d.ts +151 -0
  107. package/dist/stream-hub.js +402 -0
  108. package/dist/stream-hub.js.map +1 -0
  109. package/dist/testing.d.ts +25 -0
  110. package/dist/testing.js +19 -0
  111. package/dist/testing.js.map +1 -0
  112. package/dist/types.d.ts +221 -0
  113. package/dist/types.js +2 -0
  114. package/dist/types.js.map +1 -0
  115. package/package.json +45 -0
@@ -0,0 +1,779 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { validateAgentStreamEvent, } from '@victframework/contracts';
3
+ import { toCanonicalJson } from './serialization.js';
4
+ /**
5
+ * Stage 06B — neutral control-plane and governed-remote-execution ports.
6
+ *
7
+ * This module defines the durable records, closed state machines, and store
8
+ * ports for:
9
+ *
10
+ * - authenticated actors, roles, and scopes (SEC-001/SEC-002; default deny);
11
+ * - ChangeSets with immutable content identity, evidence, approvals,
12
+ * expiry, and forward-only status (CTRL-001..CTRL-007);
13
+ * - Application Release publish/select/rollback over immutable versions;
14
+ * - audit events for every attributable transition;
15
+ * - durable agent turns, tool invocations (durable-before-invocation), and
16
+ * VICT-authoritative approval records (AI-006/AI-007/MSTR-005);
17
+ * - the durable agent-stream ledger (per-stream monotonic sequences,
18
+ * durable milestones, transient deltas — AI-009, `vict.agent-stream@1`).
19
+ *
20
+ * In-memory reference implementations live in `control-in-memory.ts`; the
21
+ * SQLite adapter implements the same ports in `@victframework/store-sqlite` and both
22
+ * pass the shared conformance suite (`control-conformance.ts`).
23
+ *
24
+ * These are NEUTRAL ports: no Mastra, HTTP, transport, or provider type
25
+ * appears here.
26
+ */
27
+ // ---- Identifier and hashing primitives -------------------------------------
28
+ /** Bounded namespace identifier for control-plane and agent records. */
29
+ export const CONTROL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/;
30
+ /** Validate one bounded namespace identifier (throws a stable error). */
31
+ export function assertControlId(value, field) {
32
+ if (typeof value !== 'string' || !CONTROL_ID_PATTERN.test(value)) {
33
+ throw new VictControlError('VICT_CONTROL_ID_INVALID', `${field} must be a bounded namespace identifier (at most 128 characters; letters, digits, '.', '_', ':', '@', '-').`);
34
+ }
35
+ }
36
+ /** Stable bounded safe-integer validation. */
37
+ export function assertControlTimestamp(value, field) {
38
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
39
+ throw new VictControlError('VICT_CONTROL_TIMESTAMP_INVALID', `${field} must be a finite epoch-ms integer.`);
40
+ }
41
+ }
42
+ /**
43
+ * Stable content identity over strict canonical data. The canonicalizer is
44
+ * the Stage 01 verified canonical JSON; identity MUST NOT be derived from
45
+ * function text, timestamps of capture, or mutable objects.
46
+ */
47
+ export function controlContentHash(payload) {
48
+ // A versioned prefix keeps content identities domain-scoped.
49
+ return `v1_${sha256Hex(`vict.control-content@1\u0000${toCanonicalJson(payload)}`)}`;
50
+ }
51
+ /** Bounded safe string field (rationales, summaries, reason codes). */
52
+ export function assertBoundedString(value, field, max, allowEmpty = false) {
53
+ if (typeof value !== 'string' ||
54
+ (value.length === 0 && !allowEmpty) ||
55
+ value.length > max ||
56
+ // Control characters and other unsafe content are rejected; the value
57
+ // itself is never echoed into diagnostics.
58
+ // eslint-disable-next-line no-control-regex
59
+ /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value)) {
60
+ throw new Error(`VICT_CONTROL_FIELD_INVALID: ${field} must be a bounded string of at most ${max} characters without control characters.`);
61
+ }
62
+ }
63
+ function sha256Hex(payload) {
64
+ return createHash('sha256').update(payload, 'utf8').digest('hex');
65
+ }
66
+ // ---- Actors, roles, and scopes ---------------------------------------------
67
+ /** The closed reference role vocabulary (system reference §14.2). */
68
+ export const ACTOR_ROLES = [
69
+ 'viewer',
70
+ 'developer',
71
+ 'operator',
72
+ 'approver',
73
+ 'administrator',
74
+ ];
75
+ /** The closed scope vocabulary enforced below the HTTP/CLI/UI layers. */
76
+ export const ACTOR_SCOPES = [
77
+ 'changeset.propose',
78
+ 'changeset.revise',
79
+ 'changeset.approve',
80
+ 'changeset.commit',
81
+ 'changeset.read',
82
+ 'release.publish',
83
+ 'release.select',
84
+ 'release.read',
85
+ 'activation.select',
86
+ 'activation.read',
87
+ 'run.read',
88
+ 'run.cancel',
89
+ 'operator.resolve',
90
+ 'agent.turn.start',
91
+ 'agent.turn.cancel',
92
+ 'agent.tool.approve',
93
+ 'agent.tool.decline',
94
+ 'agent.stream.read',
95
+ 'app.data.read',
96
+ 'app.data.write',
97
+ 'conversation.read',
98
+ 'conversation.delete',
99
+ 'audit.read',
100
+ ];
101
+ /** Deterministic role → scope policy (default policy is denial). */
102
+ export const ROLE_SCOPES = {
103
+ viewer: ['changeset.read', 'release.read', 'activation.read', 'run.read', 'audit.read'],
104
+ developer: [
105
+ 'changeset.read',
106
+ 'changeset.propose',
107
+ 'changeset.revise',
108
+ 'release.read',
109
+ 'activation.read',
110
+ 'run.read',
111
+ 'agent.turn.start',
112
+ 'agent.turn.cancel',
113
+ 'agent.stream.read',
114
+ 'app.data.read',
115
+ 'audit.read',
116
+ ],
117
+ operator: [
118
+ 'changeset.read',
119
+ 'release.read',
120
+ 'release.select',
121
+ 'activation.read',
122
+ 'activation.select',
123
+ 'run.read',
124
+ 'run.cancel',
125
+ 'operator.resolve',
126
+ 'agent.turn.cancel',
127
+ 'agent.stream.read',
128
+ 'app.data.read',
129
+ 'app.data.write',
130
+ 'conversation.read',
131
+ 'conversation.delete',
132
+ 'audit.read',
133
+ ],
134
+ approver: [
135
+ 'changeset.read',
136
+ 'changeset.approve',
137
+ 'agent.tool.approve',
138
+ 'agent.tool.decline',
139
+ 'release.read',
140
+ 'activation.read',
141
+ 'run.read',
142
+ 'audit.read',
143
+ ],
144
+ administrator: [...ACTOR_SCOPES],
145
+ };
146
+ /**
147
+ * Derive the permitted scopes of one actor record. Unknown, disabled, and
148
+ * malformed actors fail closed: no scopes are derived. Roles outside the
149
+ * closed vocabulary are ignored (fail closed, never widened).
150
+ */
151
+ export function authoritativeScopes(actor) {
152
+ if (actor === undefined || actor.status !== 'active') {
153
+ return [];
154
+ }
155
+ const scopes = new Set();
156
+ for (const role of actor.roles) {
157
+ if (!ACTOR_ROLES.includes(role)) {
158
+ continue;
159
+ }
160
+ for (const scope of ROLE_SCOPES[role]) {
161
+ scopes.add(scope);
162
+ }
163
+ }
164
+ return [...scopes].sort();
165
+ }
166
+ /**
167
+ * Derive the authoritative server context from a resolved actor record.
168
+ * Unknown, disabled, malformed, or mismatched actors fail closed.
169
+ */
170
+ export function authenticatedActorContext(actor, requestedActorId) {
171
+ // Mismatch between the authenticated identity and any requested actor id
172
+ // is denied: the authenticated record is the only authority.
173
+ if (actor === undefined || actor.status !== 'active' || actor.actorId !== requestedActorId) {
174
+ throw new VictControlError('VICT_ACTOR_UNAUTHENTICATED', 'VICT_ACTOR_UNAUTHENTICATED: the actor could not be authenticated.');
175
+ }
176
+ assertControlId(actor.actorId, 'actorId');
177
+ return {
178
+ actorId: actor.actorId,
179
+ roles: [...actor.roles],
180
+ scopes: authoritativeScopes(actor),
181
+ mastraResourceId: `vict-actor-${actor.actorId}`,
182
+ };
183
+ }
184
+ /** Stable authorization denial (never echoes the attempted operation's payload). */
185
+ export class ActorScopeDeniedError extends Error {
186
+ code = 'VICT_ACTOR_SCOPE_DENIED';
187
+ scope;
188
+ constructor(scope) {
189
+ super(`VICT_ACTOR_SCOPE_DENIED: the authenticated actor does not hold the '${scope}' scope.`);
190
+ this.name = 'ActorScopeDeniedError';
191
+ this.scope = scope;
192
+ }
193
+ }
194
+ /** Enforce one scope (fail closed; stable non-echoing denial). */
195
+ export function assertActorScope(context, scope) {
196
+ if (!context.scopes.includes(scope)) {
197
+ throw new ActorScopeDeniedError(scope);
198
+ }
199
+ }
200
+ /** In-memory ActorDirectory (tests and local compositions). */
201
+ export class InMemoryActorDirectory {
202
+ #actors = new Map();
203
+ async get(actorId) {
204
+ const record = this.#actors.get(actorId);
205
+ return record === undefined ? undefined : { ...record, roles: [...record.roles] };
206
+ }
207
+ async list() {
208
+ return [...this.#actors.values()]
209
+ .sort((a, b) => (a.actorId < b.actorId ? -1 : 1))
210
+ .map((record) => ({ ...record, roles: [...record.roles] }));
211
+ }
212
+ async upsert(record) {
213
+ assertControlId(record.actorId, 'actorId');
214
+ if (record.status !== 'active' && record.status !== 'disabled') {
215
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'actor status must be active or disabled.');
216
+ }
217
+ if (!Array.isArray(record.roles)) {
218
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'actor roles must be an array.');
219
+ }
220
+ for (const role of record.roles) {
221
+ if (!ACTOR_ROLES.includes(role)) {
222
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'actor roles must use the closed role vocabulary.');
223
+ }
224
+ }
225
+ assertControlTimestamp(record.createdAt, 'actor.createdAt');
226
+ this.#actors.set(record.actorId, { ...record, roles: [...record.roles] });
227
+ }
228
+ }
229
+ // ---- ChangeSets --------------------------------------------------------------
230
+ /** The versioned ChangeSet schema marker. */
231
+ export const CHANGESET_SCHEMA = 'vict.changeset@1';
232
+ /** The sentinel base version meaning 'no version is currently selected'. */
233
+ export const CHANGESET_BASE_NONE = 'none';
234
+ /** Derive the stable operation identity for one ChangeSet operation. */
235
+ export function changeSetOperationIdentity(input) {
236
+ return controlContentHash({
237
+ domain: 'vict.changeset-operation@1',
238
+ changesetId: input.changesetId,
239
+ contentHash: input.contentHash,
240
+ operationIndex: input.operationIndex,
241
+ operation: input.operation,
242
+ });
243
+ }
244
+ /**
245
+ * The CLOSED structural vocabulary of one ChangeSet operation kind: every
246
+ * kind declares its EXACT required own members. A declaration that misses
247
+ * a member, adds a member, or mistypes a member is malformed and can
248
+ * never reach a content hash or a durable record.
249
+ */
250
+ const CHANGESET_OPERATION_FIELDS = {
251
+ 'select-activation': ['kind', 'graphId', 'activationVersion'],
252
+ 'rollback-activation': ['kind', 'graphId', 'targetActivationVersion'],
253
+ 'publish-and-select-release': ['kind', 'release'],
254
+ 'select-release': ['kind', 'applicationId', 'releaseVersion'],
255
+ 'rollback-release': ['kind', 'applicationId', 'targetReleaseVersion'],
256
+ };
257
+ /**
258
+ * The exact declared members of the ChangeSet base (closed structure).
259
+ */
260
+ const CHANGESET_BASE_FIELDS = ['kind', 'subjectId', 'expectedVersion'];
261
+ /**
262
+ * Capture a CLOSED plain-data record without ever invoking caller code or
263
+ * echoing captured values:
264
+ *
265
+ * - the value must be a non-null, non-array object whose prototype is
266
+ * exactly `Object.prototype` (or null) — class instances, exotic
267
+ * prototypes, and structs smuggled through exotic prototypes fail;
268
+ * - property enumeration, `Reflect.ownKeys`, and descriptor reads are
269
+ * guarded — a hostile proxy that throws is rejected without a raw
270
+ * exception crossing the boundary;
271
+ * - OWN string-keyed ENUMERABLE data properties only: accessors
272
+ * (getters/setters), inherited members, non-enumerable fields, and
273
+ * symbol keys are rejected, and getters are NEVER invoked (values are
274
+ * read exclusively through the captured property descriptor).
275
+ * - the field set must be EXACTLY the declared closed set.
276
+ *
277
+ * The returned capture is a freshly allocated plain object owned by VICT
278
+ * (the caller's object is never retained, frozen, or mutated).
279
+ */
280
+ export function captureClosedControlRecord(raw, field, allowed) {
281
+ // Every inspection primitive — including `Array.isArray`, which THROWS on
282
+ // a revoked Proxy — is guarded: no raw exception ever crosses the boundary.
283
+ let arrayLike;
284
+ try {
285
+ arrayLike = Array.isArray(raw);
286
+ }
287
+ catch {
288
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
289
+ }
290
+ if (typeof raw !== 'object' || raw === null || arrayLike) {
291
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} must be a plain object with a closed field set.`);
292
+ }
293
+ let prototype;
294
+ try {
295
+ prototype = Object.getPrototypeOf(raw);
296
+ }
297
+ catch {
298
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
299
+ }
300
+ if (prototype !== Object.prototype && prototype !== null) {
301
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} must be a plain data record; exotic prototypes are rejected.`);
302
+ }
303
+ let ownKeys;
304
+ try {
305
+ ownKeys = Reflect.ownKeys(raw);
306
+ }
307
+ catch {
308
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be enumerated; hostile containers are rejected.`);
309
+ }
310
+ const capture = {};
311
+ for (const key of ownKeys) {
312
+ if (typeof key !== 'string') {
313
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} declares a symbol key; only plain data properties are accepted.`);
314
+ }
315
+ if (allowed !== undefined && !allowed.includes(key)) {
316
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} declares a field outside the closed structure.`);
317
+ }
318
+ let descriptor;
319
+ try {
320
+ descriptor = Object.getOwnPropertyDescriptor(raw, key);
321
+ }
322
+ catch {
323
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
324
+ }
325
+ if (descriptor === undefined ||
326
+ descriptor.get !== undefined ||
327
+ descriptor.set !== undefined ||
328
+ descriptor.enumerable !== true) {
329
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} declares an accessor, inherited, or non-enumerable member; only own enumerable data properties are accepted.`);
330
+ }
331
+ capture[key] = descriptor.value;
332
+ }
333
+ for (const required of allowed ?? []) {
334
+ if (!(required in capture)) {
335
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} is missing a required member of the closed structure.`);
336
+ }
337
+ }
338
+ return capture;
339
+ }
340
+ /**
341
+ * Capture a CLOSED dense array without ever invoking caller code:
342
+ *
343
+ * - the value must be a REAL array (`Array.isArray` under a guard — a
344
+ * revoked Proxy throws there — whose prototype is exactly
345
+ * `Array.prototype`);
346
+ * - the `length` is read through its OWN DESCRIPTOR under a guard (never a
347
+ * `get`, which would be a caller-controlled trap): it must be an own
348
+ * DATA property carrying a safe integer within [1, maxLength];
349
+ * - `Reflect.ownKeys` (guarded) must declare EXACTLY the length and the
350
+ * dense index keys: extra string or symbol properties, sparse holes,
351
+ * non-enumerable indices, accessors, and hostile enumeration/descriptor
352
+ * traps are ALL rejected;
353
+ * - elements are captured exclusively through their guarded own property
354
+ * DESCRIPTORS (enumerable data properties only) — caller `.map()`,
355
+ * iterators, getters, and index `get` traps are never consulted.
356
+ *
357
+ * The returned array is freshly allocated and owned by VICT (the caller's
358
+ * array is never retained, frozen, or aliased).
359
+ */
360
+ export function captureClosedControlArray(raw, field, maxLength) {
361
+ let arrayLike;
362
+ try {
363
+ arrayLike = Array.isArray(raw);
364
+ }
365
+ catch {
366
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
367
+ }
368
+ if (!arrayLike) {
369
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} must be an array with a closed dense structure.`);
370
+ }
371
+ let prototype;
372
+ try {
373
+ prototype = Object.getPrototypeOf(raw);
374
+ }
375
+ catch {
376
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
377
+ }
378
+ if (prototype !== Array.prototype) {
379
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} must be a plain array; exotic prototypes are rejected.`);
380
+ }
381
+ // The length is inspected through its guarded own DESCRIPTOR — never a
382
+ // caller-controlled `get`.
383
+ let lengthDescriptor;
384
+ try {
385
+ lengthDescriptor = Object.getOwnPropertyDescriptor(raw, 'length');
386
+ }
387
+ catch {
388
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
389
+ }
390
+ if (lengthDescriptor === undefined ||
391
+ lengthDescriptor.get !== undefined ||
392
+ lengthDescriptor.set !== undefined ||
393
+ typeof lengthDescriptor.value !== 'number' ||
394
+ !Number.isSafeInteger(lengthDescriptor.value) ||
395
+ lengthDescriptor.value < 1 ||
396
+ lengthDescriptor.value > maxLength) {
397
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} length is outside the closed bounded structure.`);
398
+ }
399
+ const length = lengthDescriptor.value;
400
+ // The declared key set must be EXACTLY the dense index set plus length.
401
+ let ownKeys;
402
+ try {
403
+ ownKeys = Reflect.ownKeys(raw);
404
+ }
405
+ catch {
406
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be enumerated; hostile containers are rejected.`);
407
+ }
408
+ const expectedKeys = new Set(['length']);
409
+ for (let index = 0; index < length; index += 1) {
410
+ expectedKeys.add(String(index));
411
+ }
412
+ if (ownKeys.length !== expectedKeys.size) {
413
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} declares members outside the closed dense structure.`);
414
+ }
415
+ for (const key of ownKeys) {
416
+ if (!expectedKeys.has(key)) {
417
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} declares a member outside the closed dense structure.`);
418
+ }
419
+ }
420
+ // Dense capture: every index must be an OWN ENUMERABLE DATA property read
421
+ // through its guarded descriptor (accessors, holes, and non-enumerable
422
+ // indices are rejected; caller getters are never invoked).
423
+ const capture = new Array(length);
424
+ for (let index = 0; index < length; index += 1) {
425
+ let descriptor;
426
+ try {
427
+ descriptor = Object.getOwnPropertyDescriptor(raw, String(index));
428
+ }
429
+ catch {
430
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} could not be inspected; hostile containers are rejected.`);
431
+ }
432
+ if (descriptor === undefined ||
433
+ descriptor.get !== undefined ||
434
+ descriptor.set !== undefined ||
435
+ descriptor.enumerable !== true) {
436
+ throw new VictControlError('VICT_CONTROL_STRUCTURE_INVALID', `The ${field} must be a dense array of own enumerable data elements.`);
437
+ }
438
+ capture[index] = descriptor.value;
439
+ }
440
+ return capture;
441
+ }
442
+ /** Validate a required bounded string identifier member (exact type). */
443
+ function requireBoundedMember(capture, field) {
444
+ const value = capture[field];
445
+ if (typeof value !== 'string') {
446
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', `The member '${field}' must be a string.`);
447
+ }
448
+ assertControlId(value, field);
449
+ return value;
450
+ }
451
+ /**
452
+ * Validate the structural shape of one ChangeSet operation (fail closed).
453
+ *
454
+ * EVERY required member of the operation kind is enforced with its exact
455
+ * runtime type, non-empty bounded identifier form, and the closed own-field
456
+ * set. Accessors, inherited/non-enumerable members, symbols, exotic
457
+ * prototypes, and hostile proxies are rejected WITHOUT invoking getters or
458
+ * echoing captured values. Only the validated VICT-owned canonical capture
459
+ * is returned/hashed — never the original caller object.
460
+ */
461
+ export function validateChangeSetOperation(operation) {
462
+ // Structural capture FIRST (closed plain-data discipline, no field-set
463
+ // restriction yet) to read the kind identity WITHOUT invoking getters.
464
+ const structure = captureClosedControlRecord(operation, 'ChangeSet operation', undefined);
465
+ if (typeof structure.kind !== 'string' || !(structure.kind in CHANGESET_OPERATION_FIELDS)) {
466
+ throw new VictControlError('VICT_CONTROL_OPERATION_INVALID', 'The ChangeSet operation kind is outside the closed vocabulary.');
467
+ }
468
+ const kindCapture = structure.kind;
469
+ const allowed = CHANGESET_OPERATION_FIELDS[kindCapture];
470
+ // Re-capture with the EXACT closed field set for the resolved kind.
471
+ const capture = captureClosedControlRecord(operation, 'ChangeSet operation', allowed);
472
+ if (capture.kind !== kindCapture) {
473
+ throw new VictControlError('VICT_CONTROL_OPERATION_INVALID', 'The ChangeSet operation kind must be a stable string identity.');
474
+ }
475
+ switch (kindCapture) {
476
+ case 'select-activation':
477
+ return {
478
+ kind: 'select-activation',
479
+ graphId: requireBoundedMember(capture, 'graphId'),
480
+ activationVersion: requireBoundedMember(capture, 'activationVersion'),
481
+ };
482
+ case 'rollback-activation':
483
+ return {
484
+ kind: 'rollback-activation',
485
+ graphId: requireBoundedMember(capture, 'graphId'),
486
+ targetActivationVersion: requireBoundedMember(capture, 'targetActivationVersion'),
487
+ };
488
+ case 'publish-and-select-release':
489
+ return {
490
+ kind: 'publish-and-select-release',
491
+ release: validateApplicationReleaseContent(capture.release),
492
+ };
493
+ case 'select-release':
494
+ return {
495
+ kind: 'select-release',
496
+ applicationId: requireBoundedMember(capture, 'applicationId'),
497
+ releaseVersion: requireBoundedMember(capture, 'releaseVersion'),
498
+ };
499
+ case 'rollback-release':
500
+ return {
501
+ kind: 'rollback-release',
502
+ applicationId: requireBoundedMember(capture, 'applicationId'),
503
+ targetReleaseVersion: requireBoundedMember(capture, 'targetReleaseVersion'),
504
+ };
505
+ default:
506
+ throw new VictControlError('VICT_CONTROL_OPERATION_INVALID', 'The ChangeSet operation kind is outside the closed vocabulary.');
507
+ }
508
+ }
509
+ /** The closed immutable release content fields. */
510
+ const RELEASE_CONTENT_FIELDS = [
511
+ 'releaseVersion',
512
+ 'applicationId',
513
+ 'applicationVersion',
514
+ 'rendererIdentity',
515
+ 'componentRegistryIdentity',
516
+ 'dataAdapterIdentity',
517
+ 'activationBinding',
518
+ ];
519
+ /**
520
+ * Validate one immutable Application Release content record (fail closed).
521
+ * The content is captured as a CLOSED plain-data structure (exact member
522
+ * set, exact string types, no accessors/symbols/hostile containers) and
523
+ * the returned record is a fresh VICT-owned capture.
524
+ */
525
+ export function validateApplicationReleaseContent(content) {
526
+ const capture = captureClosedControlRecord(content, 'release content', RELEASE_CONTENT_FIELDS);
527
+ for (const field of RELEASE_CONTENT_FIELDS) {
528
+ const value = capture[field];
529
+ if (typeof value !== 'string') {
530
+ throw new VictControlError('VICT_CONTROL_RELEASE_INVALID', `The release member '${field}' must be a string.`);
531
+ }
532
+ assertControlId(value, `release.${field}`);
533
+ }
534
+ return {
535
+ releaseVersion: capture.releaseVersion,
536
+ applicationId: capture.applicationId,
537
+ applicationVersion: capture.applicationVersion,
538
+ rendererIdentity: capture.rendererIdentity,
539
+ componentRegistryIdentity: capture.componentRegistryIdentity,
540
+ dataAdapterIdentity: capture.dataAdapterIdentity,
541
+ activationBinding: capture.activationBinding,
542
+ };
543
+ }
544
+ /** The exact declared members of the ChangeSet authoring input envelope. */
545
+ const CHANGESET_CONTENT_INPUT_FIELDS = [
546
+ 'changesetId',
547
+ 'authorActorId',
548
+ 'createdAt',
549
+ 'base',
550
+ 'operations',
551
+ 'rationale',
552
+ 'riskClass',
553
+ 'requiredApproverCount',
554
+ 'expiresAt',
555
+ ];
556
+ /** Validate a full ChangeSet authoring input and derive its content hash.
557
+ *
558
+ * The COMPLETE untrusted runtime input is CAPTURED at this boundary before
559
+ * any member is read: the outer envelope must be a plain object with the
560
+ * EXACT closed field set, and `base`, the operation list, and every
561
+ * operation are captured through guarded descriptors (no caller getter,
562
+ * iterator, `.map()`, or other caller-controlled behavior is ever
563
+ * consulted; hostile/revoked proxies, sparse arrays, accessors, symbols,
564
+ * non-enumerable members, and exotic prototypes are all rejected with ONE
565
+ * stable, non-echoing error). Only VICT-owned validated captures are
566
+ * hashed and returned; caller objects are never retained, frozen, or aliased.
567
+ */
568
+ export function validateChangeSetContent(untrustedInput) {
569
+ // ---- 1. CAPTURE the complete outer envelope (closed field set) BEFORE
570
+ // any semantic inspection. Scalar members are captured by descriptor
571
+ // value too, so caller getters never run (a getter on ANY outer field —
572
+ // not only `base` — is rejected without being invoked).
573
+ const input = captureClosedControlRecord(untrustedInput, 'ChangeSet authoring input', CHANGESET_CONTENT_INPUT_FIELDS);
574
+ if (typeof input.changesetId !== 'string') {
575
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'the ChangeSet id is malformed.');
576
+ }
577
+ if (typeof input.authorActorId !== 'string') {
578
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'the ChangeSet author is malformed.');
579
+ }
580
+ if (typeof input.createdAt !== 'number' ||
581
+ !Number.isSafeInteger(input.createdAt) ||
582
+ typeof input.expiresAt !== 'number' ||
583
+ !Number.isSafeInteger(input.expiresAt)) {
584
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'the ChangeSet timestamps are malformed.');
585
+ }
586
+ assertControlId(input.changesetId, 'changesetId');
587
+ assertControlId(input.authorActorId, 'authorActorId');
588
+ assertControlTimestamp(input.createdAt, 'createdAt');
589
+ assertControlTimestamp(input.expiresAt, 'expiresAt');
590
+ if (input.expiresAt <= input.createdAt) {
591
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'a ChangeSet must expire after its creation.');
592
+ }
593
+ // ---- 2. The base is CAPTURED (closed plain-data structure: exactly its
594
+ // declared members, exact string types, no accessors/symbols/hostile
595
+ // containers) before semantic inspection — a hostile `base.kind` getter
596
+ // is never invoked and a revoked proxy never escapes a raw TypeError.
597
+ const baseCapture = captureClosedControlRecord(input.base, 'ChangeSet base', CHANGESET_BASE_FIELDS);
598
+ if (baseCapture.kind !== 'activation' && baseCapture.kind !== 'release') {
599
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'the ChangeSet base is malformed.');
600
+ }
601
+ if (typeof baseCapture.subjectId !== 'string' ||
602
+ typeof baseCapture.expectedVersion !== 'string') {
603
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'the ChangeSet base is malformed.');
604
+ }
605
+ assertControlId(baseCapture.subjectId, 'base.subjectId');
606
+ assertControlId(baseCapture.expectedVersion, 'base.expectedVersion');
607
+ const base = {
608
+ kind: baseCapture.kind,
609
+ subjectId: baseCapture.subjectId,
610
+ expectedVersion: baseCapture.expectedVersion,
611
+ };
612
+ // ---- 3. The operation list is CAPTURED as a closed dense array through
613
+ // guarded descriptors (never caller `.map()`/iterators); sparse arrays,
614
+ // extra/symbol properties, accessor or non-enumerable indices, exotic
615
+ // prototypes, and revoked/hostile proxies all fail with ONE stable error.
616
+ const operationsCapture = captureClosedControlArray(input.operations, 'ChangeSet operation list', 64);
617
+ const operations = [];
618
+ for (const operation of operationsCapture) {
619
+ operations.push(validateChangeSetOperation(operation));
620
+ }
621
+ if (typeof input.rationale !== 'string') {
622
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'the rationale is malformed.');
623
+ }
624
+ assertBoundedString(input.rationale, 'rationale', 2000);
625
+ if (input.riskClass !== 'low' && input.riskClass !== 'medium' && input.riskClass !== 'high') {
626
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'riskClass must be low, medium, or high.');
627
+ }
628
+ if (typeof input.requiredApproverCount !== 'number' ||
629
+ !Number.isSafeInteger(input.requiredApproverCount) ||
630
+ input.requiredApproverCount < 1 ||
631
+ input.requiredApproverCount > 8) {
632
+ throw new VictControlError('VICT_CONTROL_FIELD_INVALID', 'requiredApproverCount must be a safe integer between 1 and 8.');
633
+ }
634
+ // The content identity is derived from the VALIDATED VICT-owned captures
635
+ // (the closed base structure and the canonical operation set) — never from
636
+ // the original caller objects (which could carry accessors, symbols, or
637
+ // exotic prototypes and are never retained).
638
+ const contentHash = controlContentHash({
639
+ schema: CHANGESET_SCHEMA,
640
+ authorActorId: input.authorActorId,
641
+ base,
642
+ operations,
643
+ rationale: input.rationale,
644
+ riskClass: input.riskClass,
645
+ requiredApproverCount: input.requiredApproverCount,
646
+ expiresAt: input.expiresAt,
647
+ });
648
+ return {
649
+ changesetId: input.changesetId,
650
+ base,
651
+ operations,
652
+ rationale: input.rationale,
653
+ riskClass: input.riskClass,
654
+ requiredApproverCount: input.requiredApproverCount,
655
+ expiresAt: input.expiresAt,
656
+ contentHash,
657
+ };
658
+ }
659
+ void undefined;
660
+ /** The closed audit action vocabulary. */
661
+ export const CONTROL_AUDIT_ACTIONS = [
662
+ 'changeset.proposed',
663
+ 'changeset.revised',
664
+ 'changeset.evidence-attached',
665
+ 'changeset.approved',
666
+ 'changeset.declined',
667
+ 'changeset.committed',
668
+ 'changeset.expired',
669
+ 'release.published',
670
+ 'release.selected',
671
+ 'release.rolled-back',
672
+ 'activation.selected',
673
+ 'activation.rolled-back',
674
+ 'actor.recorded',
675
+ 'run.cancelled',
676
+ 'turn.cancelled',
677
+ 'approval.decided',
678
+ 'approval.expired',
679
+ 'operator.intervened',
680
+ ];
681
+ // ---- Agent turns ------------------------------------------------------------
682
+ export const AGENT_TURN_SCHEMA = 'vict.agent-turn@1';
683
+ /** Durable kinds that a ledger row persists (transient kinds never persist). */
684
+ export function isDurableStreamKind(kind) {
685
+ return kind !== 'text.delta';
686
+ }
687
+ // ---- The composed control-plane store set -------------------------------------
688
+ // ---- Durable command idempotency -----------------------------------------
689
+ /** The closed, bounded idempotency-key format (HTTP + CLI surface). */
690
+ export const COMMAND_IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
691
+ /** Stable structured conflict code thrown when a settlement fence mismatches. */
692
+ export const VICT_IDEMPOTENCY_FENCE_CONFLICT = 'VICT_IDEMPOTENCY_FENCE_CONFLICT';
693
+ /**
694
+ * Derive the deterministic settlement fence token for one claim
695
+ * generation. The token binds the namespace, the lease owner, and the
696
+ * attempt generation: a lease takeover always produces a NEW token, so a
697
+ * stale owner can never settle a claim it no longer owns.
698
+ */
699
+ export function commandIdempotencyFenceToken(input) {
700
+ return ('vict-fence-' +
701
+ createHash('sha256')
702
+ .update(`${input.actorId}\u0000${input.command}\u0000${input.idempotencyKey}\u0000${input.owner}\u0000${input.attempts}`, 'utf8')
703
+ .digest('hex'));
704
+ }
705
+ /** Validation error for any control-plane structural violation. */
706
+ export class VictControlError extends Error {
707
+ code;
708
+ constructor(code, message) {
709
+ super(message);
710
+ this.name = 'VictControlError';
711
+ this.code = code;
712
+ }
713
+ }
714
+ /** Canonical JSON of one stream event's safe payload fields (context minus stream identity). */
715
+ export function streamEventPayloadOf(event) {
716
+ const payload = {};
717
+ for (const [key, value] of Object.entries(event)) {
718
+ if (key === 'streamId' || key === 'seq' || key === 'createdAt') {
719
+ continue;
720
+ }
721
+ payload[key] = value;
722
+ }
723
+ return toCanonicalJson(payload);
724
+ }
725
+ // ---- The stream-ledger append gate (store-boundary schema enforcement) ------
726
+ /**
727
+ * Validate ONE append against the final `vict.agent-stream@1` schema at
728
+ * the STORE boundary — before any sequence state is incremented or any
729
+ * storage mutated. The RAW ledger ports are the last line of defense:
730
+ * plain-JavaScript callers cannot bypass the schema through them.
731
+ *
732
+ * Enforced here:
733
+ * - the payload is a JSON object in CANONICAL form (non-canonical JSON is
734
+ * rejected, not silently re-serialized);
735
+ * - the declared kind is inside the closed vocabulary and matches the
736
+ * payload's `kind` member;
737
+ * - the reconstructed event (payload + stream identity + the sequence that
738
+ * WOULD be assigned) passes the complete field-level schema (unknown
739
+ * kinds/fields, malformed correlation IDs, unsafe codes, raw text in
740
+ * `content.completed`, invalid usage — all fail closed).
741
+ *
742
+ * Errors are stable and non-echoing: `VICT_STREAM_EVENT_INVALID` carries
743
+ * schema issue CODES only, never the rejected values.
744
+ */
745
+ export function validateStreamLedgerAppend(command) {
746
+ assertControlId(command.streamId, 'streamId');
747
+ let parsed;
748
+ try {
749
+ parsed = JSON.parse(command.payload);
750
+ }
751
+ catch {
752
+ throw new VictControlError('VICT_STREAM_EVENT_INVALID', 'The stream event payload is not valid JSON; the ledger rejects it without persisting anything.');
753
+ }
754
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
755
+ throw new VictControlError('VICT_STREAM_EVENT_INVALID', 'The stream event payload must be a JSON object.');
756
+ }
757
+ // Canonical-form enforcement: the stored payload must be the canonical
758
+ // serialization of its own content (no key-order drift, no whitespace).
759
+ if (toCanonicalJson(parsed) !== command.payload) {
760
+ throw new VictControlError('VICT_STREAM_EVENT_INVALID', 'The stream event payload is not canonical JSON; persisting it would break byte-stable replay.');
761
+ }
762
+ const candidate = parsed;
763
+ if (candidate['kind'] !== command.kind) {
764
+ throw new VictControlError('VICT_STREAM_EVENT_INVALID', 'The payload kind does not match the declared event kind.');
765
+ }
766
+ const event = {
767
+ ...candidate,
768
+ streamId: command.streamId,
769
+ seq: command.assignedSeq,
770
+ };
771
+ const validation = validateAgentStreamEvent(event);
772
+ if (!validation.ok) {
773
+ throw new VictControlError('VICT_STREAM_EVENT_INVALID', `The event is not a valid vict.agent-stream@1 event (${validation.issues
774
+ .map((issue) => issue.code)
775
+ .join(', ')}).`);
776
+ }
777
+ return event;
778
+ }
779
+ //# sourceMappingURL=control-types.js.map