@awebai/oats 0.23.2 → 0.24.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 +7 -4
- package/bin/oats-pi-sdk-host.mjs +17 -0
- package/bin/oats.mjs +442 -51
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
- package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
- package/capabilities/oats-okf/lib/captured-worker.mjs +101 -0
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/inspection.mjs +16 -1
- package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
- package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
- package/capabilities/oats-okf/lib/io.mjs +1 -1
- package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
- package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
- package/capabilities/oats-okf/lib/sources.mjs +123 -3
- package/capabilities/oats-okf/lib/stores.mjs +104 -25
- package/capabilities/oats-okf/lib/worker.mjs +69 -10
- package/capabilities/oats-okf/oats.json +35 -7
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
- package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
- package/docs/artifact-approvals.schema.json +7 -0
- package/docs/capability-manifest.schema.json +37 -66
- package/docs/captured-invocation-context.schema.json +7 -0
- package/docs/captured-resolution.schema.json +7 -0
- package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
- package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
- package/docs/design/2026-09-15-captured-dispatch.md +127 -0
- package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
- package/docs/design/2026-09-15-package-preparation.md +100 -0
- package/docs/design/2026-09-15-portable-data-contract.md +121 -0
- package/docs/design/2026-09-15-portable-declarations.md +189 -0
- package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
- package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
- package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
- package/docs/design/2026-09-15-source-observation.md +119 -0
- package/docs/design/2026-09-16-captured-admission.md +77 -0
- package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
- package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
- package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
- package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
- package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
- package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
- package/docs/design/2026-09-16-portable-onboarding.md +177 -0
- package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
- package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
- package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
- package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
- package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
- package/docs/design/2026-09-17-captured-native-start.md +58 -0
- package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
- package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
- package/docs/design/2026-09-17-public-captured-start.md +108 -0
- package/docs/design/2026-09-17-public-prepare-request.md +90 -0
- package/docs/design/2026-09-18-captured-pi-host.md +205 -0
- package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
- package/docs/desktop-cli-api.md +5 -2
- package/docs/execution-capsule.schema.json +108 -0
- package/docs/execution-targets.md +20 -7
- package/docs/oats-lock-v3.schema.json +7 -0
- package/docs/oats-member.schema.json +38 -0
- package/docs/oats-workspace.schema.json +68 -0
- package/docs/portable.schema.json +2512 -0
- package/docs/provider-check-input.schema.json +7 -0
- package/docs/release-notes/v0.24.0.md +104 -0
- package/docs/schedules.md +126 -14
- package/docs/soul.schema.json +82 -0
- package/injects/oats-portable.md +17 -0
- package/injects/portable-instance-boundary.md +39 -0
- package/injects/portable-work-directory.md +29 -0
- package/lib/artifact-approvals.mjs +120 -0
- package/lib/artifact-tree.mjs +141 -0
- package/lib/capability-artifacts.mjs +179 -0
- package/lib/capability-execution.mjs +15 -0
- package/lib/capability-inputs.mjs +39 -0
- package/lib/capability-provenance.mjs +231 -0
- package/lib/captured-action-shape.mjs +21 -0
- package/lib/captured-admission-shape.mjs +20 -0
- package/lib/captured-binding-file.mjs +36 -0
- package/lib/captured-dispatch.mjs +66 -0
- package/lib/captured-instance-index.mjs +277 -0
- package/lib/captured-invocation-context.mjs +130 -0
- package/lib/captured-launch-request.mjs +46 -0
- package/lib/captured-operation-process.mjs +15 -0
- package/lib/captured-pi-custody.mjs +29 -0
- package/lib/captured-pi-host.mjs +167 -0
- package/lib/captured-pi-outcome.mjs +172 -0
- package/lib/captured-resolutions.mjs +275 -0
- package/lib/captured-scaffold.mjs +87 -0
- package/lib/captured-selector.mjs +28 -0
- package/lib/captured-session-backend.mjs +52 -0
- package/lib/captured-source-receipt-file.mjs +72 -0
- package/lib/config-data.mjs +104 -0
- package/lib/core.mjs +918 -562
- package/lib/errors.mjs +7 -0
- package/lib/helper-injection-policy.mjs +98 -0
- package/lib/herdr.mjs +18 -7
- package/lib/instruction-composition.mjs +31 -0
- package/lib/legacy-lock-codec.mjs +106 -0
- package/lib/manifest-settings.mjs +84 -0
- package/lib/package-closure.mjs +48 -0
- package/lib/package-materialization.mjs +83 -0
- package/lib/pi-sdk-host.mjs +229 -0
- package/lib/portable-artifacts.mjs +115 -0
- package/lib/portable-choices.mjs +82 -0
- package/lib/portable-composition.mjs +136 -0
- package/lib/portable-digest.mjs +105 -0
- package/lib/portable-files.mjs +26 -0
- package/lib/portable-identity.mjs +40 -0
- package/lib/portable-lock.mjs +117 -0
- package/lib/portable-migration-artifacts.mjs +135 -0
- package/lib/portable-migration-evidence.mjs +305 -0
- package/lib/portable-migration-store.mjs +199 -0
- package/lib/portable-migration.mjs +104 -0
- package/lib/portable-onboarding-acceptance.mjs +66 -0
- package/lib/portable-onboarding-request.mjs +49 -0
- package/lib/portable-onboarding.mjs +230 -0
- package/lib/portable-package-preparation.mjs +188 -0
- package/lib/portable-policy.mjs +44 -0
- package/lib/portable-shape.mjs +35 -0
- package/lib/portable-soul.mjs +38 -0
- package/lib/portable-state.mjs +80 -0
- package/lib/portable-values.mjs +181 -0
- package/lib/prepare-composition.mjs +151 -0
- package/lib/prepared-bindings.mjs +78 -0
- package/lib/prepared-resources.mjs +127 -0
- package/lib/provider-binding-broker.mjs +59 -0
- package/lib/provider-binding-wire.mjs +110 -0
- package/lib/provider-binding.mjs +22 -0
- package/lib/repository-observation.mjs +226 -0
- package/lib/resolution-shape.mjs +393 -0
- package/lib/schedule-capsule.mjs +206 -0
- package/lib/schedule.mjs +259 -38
- package/lib/servers.mjs +15 -0
- package/lib/soul-constraints.mjs +40 -0
- package/lib/source-projection.mjs +84 -0
- package/lib/source-spec.mjs +189 -0
- package/lib/workspace-definition.mjs +126 -0
- package/lib/workspace-discovery.mjs +146 -0
- package/package-catalog.json +1 -1
- package/package.json +3 -2
- package/packages/record/lib/capture-cc.mjs +14 -6
- package/packages/record/lib/formats.mjs +14 -3
- package/packages/record/lib/native-history.mjs +277 -7
- package/packages/record/lib/session-snapshot.mjs +25 -5
- package/packages/record/lib/sessions-for-home.mjs +30 -13
- package/skills/oats/SKILL.md +12 -7
- package/skills/oats-config/SKILL.md +12 -9
- package/skills/oats-packages/SKILL.md +12 -8
- package/skills/oats-portable/SKILL.md +116 -0
- package/skills/oats-portable-artifacts/SKILL.md +63 -0
- package/skills/oats-portable-setup/SKILL.md +69 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Private invocation projection, not a new binding store or action permit.
|
|
2
|
+
* The synchronous caller has already loaded/qualified its exact captured action. */
|
|
3
|
+
import { lstatSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { portableStateDirectory } from './portable-state.mjs';
|
|
6
|
+
import { canonicalJson } from './portable-values.mjs';
|
|
7
|
+
import { validateProviderBinding } from './resolution-shape.mjs';
|
|
8
|
+
import { BINDING_LIMITS } from './provider-binding-wire.mjs';
|
|
9
|
+
import { oatsError } from './errors.mjs';
|
|
10
|
+
|
|
11
|
+
export function withCapturedBindingFile({deployment,record,capability},run) {
|
|
12
|
+
const bindings=Object.values(record.bindings).filter(binding=>binding.capability===capability.id);
|
|
13
|
+
if (!bindings.length) return run({});
|
|
14
|
+
if (bindings.length !== 1) throw oatsError('invalid-resolution','one capability cannot consume contradictory slot bindings');
|
|
15
|
+
const binding=validateProviderBinding(bindings[0]),bytes=canonicalJson(binding,BINDING_LIMITS);
|
|
16
|
+
const directory=mkdtempSync(join(portableStateDirectory(deployment,true),'.binding-'));
|
|
17
|
+
const owned=lstatSync(directory),file=join(directory,'binding.json');let primary,result,completed=false;
|
|
18
|
+
try {
|
|
19
|
+
writeFileSync(file,bytes,{flag:'wx',mode:0o600});
|
|
20
|
+
result=run({OATS_BINDING_FILE:file});completed=true;return result;
|
|
21
|
+
} catch(error) {primary=error;throw error;}
|
|
22
|
+
finally {
|
|
23
|
+
try {
|
|
24
|
+
const current=lstatSync(directory);
|
|
25
|
+
if (!current.isDirectory() || current.dev!==owned.dev || current.ino!==owned.ino) throw oatsError('integrity-drift','binding invocation directory ownership changed');
|
|
26
|
+
rmSync(directory,{recursive:true});
|
|
27
|
+
} catch(cleanup) {
|
|
28
|
+
if (!primary) {
|
|
29
|
+
const error=new AggregateError([cleanup],'captured binding cleanup failed after invocation',{cause:cleanup});
|
|
30
|
+
error.code=cleanup.code;error.invocationCompleted=completed;error.invocationResult=result;throw error;
|
|
31
|
+
}
|
|
32
|
+
const error=new AggregateError([primary,cleanup],'captured command and binding cleanup failed',{cause:primary});
|
|
33
|
+
error.code=primary.code;if(primary.invocationCompleted){error.invocationCompleted=true;error.invocationResult=primary.invocationResult;}throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** One exact action-loading boundary. No current config, lock or marketplace
|
|
2
|
+
* lookup. Core supplies its complete codecs and native host checks. */
|
|
3
|
+
import { canonicalJson } from "./portable-values.mjs";
|
|
4
|
+
import { validateCapturedAction } from "./captured-action-shape.mjs";
|
|
5
|
+
import { verifyResolutionInputs } from "./captured-resolutions.mjs";
|
|
6
|
+
import { readApprovalLedger, evaluateCapturedApprovals } from "./artifact-approvals.mjs";
|
|
7
|
+
import { composeCapturedInstructions } from "./instruction-composition.mjs";
|
|
8
|
+
import { oatsError } from "./errors.mjs";
|
|
9
|
+
|
|
10
|
+
export function loadCapturedAction({ deployment, resolution, action }, codecs) {
|
|
11
|
+
validateCapturedAction(action);
|
|
12
|
+
const verified = verifyResolutionInputs(deployment, resolution), { record } = verified;
|
|
13
|
+
if (record.capture !== "prepared") throw oatsError("migration-required", "reconstructed dispatch requires the dedicated migration evidence verifier");
|
|
14
|
+
const manifests = new Map(), capabilities = new Map();
|
|
15
|
+
for (const [id, row] of Object.entries(record.artifacts.capabilities)) {
|
|
16
|
+
const root = verified.roots.get(canonicalJson(row.artifact));
|
|
17
|
+
const manifest = codecs.manifest(root);
|
|
18
|
+
if (!manifest || manifest.capability !== id || manifest.version !== row.version) throw oatsError("invalid-resolution", "compiled manifest differs from captured identity/version");
|
|
19
|
+
const settings = Object.fromEntries(Object.entries(record.dispatch.settingsChoices[id] ?? {}).map(([name, key]) => [name, record.choices[key].value]));
|
|
20
|
+
codecs.settings(manifest, settings);
|
|
21
|
+
manifests.set(id, manifest); capabilities.set(id, { id, manifest, settings });
|
|
22
|
+
}
|
|
23
|
+
// Complete static validation applies even to inspection; neither inspection
|
|
24
|
+
// nor input verification is an excuse to accept an unsafe launch recipe.
|
|
25
|
+
if (record.dispatch.launch !== null) codecs.launch(record.dispatch.launch);
|
|
26
|
+
const approvals = evaluateCapturedApprovals({ ...verified, manifests }, readApprovalLedger(deployment).ledger);
|
|
27
|
+
const base = { ...verified, manifests, capabilities, approvals, resolution, deployment };
|
|
28
|
+
if (action.kind === "inspect") return base;
|
|
29
|
+
if (action.kind === "compose") return { ...base, composition: composeCapturedInstructions(verified) };
|
|
30
|
+
let id = action.capability;
|
|
31
|
+
if (action.kind === "operation") id = record.bindings[action.slot]?.capability;
|
|
32
|
+
if (action.namespace !== undefined) {
|
|
33
|
+
const matches = [...manifests].filter(([, manifest]) => manifest.command === action.namespace);
|
|
34
|
+
if (matches.length > 1) throw oatsError("duplicate-namespace", "captured command namespace is ambiguous");
|
|
35
|
+
id = matches[0]?.[0];
|
|
36
|
+
}
|
|
37
|
+
if (typeof id !== "string" || !capabilities.has(id)) throw oatsError("capability-not-selected", "action capability is not selected by this resolution");
|
|
38
|
+
const capability = capabilities.get(id), manifest = capability.manifest;
|
|
39
|
+
let name = action.name, operation;
|
|
40
|
+
if (action.kind === "operation") {
|
|
41
|
+
operation = codecs.operations(manifest).find((entry) => entry.name === name);
|
|
42
|
+
if (!operation) throw oatsError("operation-not-found", "captured provider does not declare this operation");
|
|
43
|
+
name = operation.command;
|
|
44
|
+
}
|
|
45
|
+
const declarations = action.kind === "hook" ? codecs.hooks(manifest) : manifest.commands;
|
|
46
|
+
if (!declarations || !Object.hasOwn(declarations, name)) throw oatsError("command-not-found", "captured capability does not declare this action");
|
|
47
|
+
const approval = approvals.find((entry) => entry.artifact.capability === id);
|
|
48
|
+
if (approval.status !== "approved") throw oatsError("approval-required", "action needs current approval for its exact capability artifact");
|
|
49
|
+
// Provider-owned payload/credential/member qualification is a separate ABI.
|
|
50
|
+
// Until its adapter is supplied, report the missing gate rather than execute
|
|
51
|
+
// using an ambient binding file or treating captured choices as enrollment.
|
|
52
|
+
const invocation = typeof codecs.invocation === "function" ? codecs.invocation(base, action, capability) : undefined;
|
|
53
|
+
const bindings = Object.entries(record.bindings).filter(([, binding]) => binding.capability === id);
|
|
54
|
+
if (bindings.length) {
|
|
55
|
+
if (typeof codecs.bindings !== "function") throw oatsError("provider-not-qualified", "captured provider binding qualification is unavailable");
|
|
56
|
+
codecs.bindings(bindings, capability, record, invocation);
|
|
57
|
+
}
|
|
58
|
+
const missing = codecs.host(capability);
|
|
59
|
+
if (missing.length) throw oatsError("host-requirement-missing", "captured action has unmet host command requirements", missing);
|
|
60
|
+
const spec = declarations[name];
|
|
61
|
+
if (typeof spec !== "string" || !spec.trim() || spec.includes("\0")) throw oatsError("invalid-resolution", "captured executable declaration is not valid text");
|
|
62
|
+
const [script, ...args] = spec.trim().split(/\s+/), file = codecs.executable(manifest, script);
|
|
63
|
+
if (!file) throw oatsError("resource-not-found", "captured executable is absent");
|
|
64
|
+
if (![...verified.resources.values()].includes(file)) throw oatsError("resolution-incomplete", "captured executable is absent from the resource inventory");
|
|
65
|
+
return { ...base, action, capability, operation, executable: { file, args }, ...(invocation ? { invocation } : {}) };
|
|
66
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/** Bounded lifecycle custody in the existing deployment index. No source/config
|
|
2
|
+
* discovery, user database, provider policy or automatic legacy backfill. */
|
|
3
|
+
import { closeSync, constants, fsyncSync, lstatSync, openSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { readPortableBytes } from "./portable-files.mjs";
|
|
8
|
+
import { compareUtf8, canonicalJson, parseStrictJson } from "./portable-values.mjs";
|
|
9
|
+
import { jsonIntegrity } from "./portable-digest.mjs";
|
|
10
|
+
import { portableScope, portableStateDirectory, withPortableStateWrite, writeGuardedPortableDocument } from "./portable-state.mjs";
|
|
11
|
+
import { validateExecutionBinding } from "./schedule-capsule.mjs";
|
|
12
|
+
import { validateIncarnationId, validateIntentRef } from "./captured-admission-shape.mjs";
|
|
13
|
+
import { validateCapturedAction } from "./captured-action-shape.mjs";
|
|
14
|
+
import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
|
|
15
|
+
import { objectAt, stringAt } from "./portable-shape.mjs";
|
|
16
|
+
import { oatsError } from "./errors.mjs";
|
|
17
|
+
|
|
18
|
+
const FILE = "instance-references.json";
|
|
19
|
+
const STATUSES = new Set(["scaffolded-hooks-pending", "spawn-hooks-running", "spawned-launch-pending", "spawned-cleanup-required", "spawn-failed-cleanup-required", "retire-running", "retire-failed-cleanup-required", "start-running", "start-dispatched", "start-failed-cleanup-required"]);
|
|
20
|
+
const INTENT_STATES = new Set(["admitted", "running", "completed", "unconfirmed", "blocked"]);
|
|
21
|
+
const LIMITS = Object.freeze({ maxBytes: 4 * 1024 * 1024, maxDepth: 32, maxEntries: 40_000 });
|
|
22
|
+
const RECEIPT_LIMITS = Object.freeze({ maxBytes: 128 * 1024, maxDepth: 24, maxEntries: 8192 });
|
|
23
|
+
const same = (a,b) => canonicalJson(a) === canonicalJson(b);
|
|
24
|
+
|
|
25
|
+
export function capturedDirectoryIdentity(path) {
|
|
26
|
+
const stat = lstatSync(path);
|
|
27
|
+
if (!stat.isDirectory()) throw oatsError("integrity-drift", "captured home/work must remain real directories");
|
|
28
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
29
|
+
}
|
|
30
|
+
function directoryIdentity(value) {
|
|
31
|
+
objectAt(value,["dev","ino"],["dev","ino"]);
|
|
32
|
+
for (const key of ["dev","ino"]) if (!Number.isSafeInteger(value[key]) || value[key] < 0) throw oatsError("invalid-declaration","invalid directory identity witness");
|
|
33
|
+
}
|
|
34
|
+
function human(value) {
|
|
35
|
+
if (value === null) return;
|
|
36
|
+
objectAt(value,["provider","id"],["provider","id"]);stringAt(value.provider,"/human/provider");stringAt(value.id,"/human/id");
|
|
37
|
+
}
|
|
38
|
+
function intentRef(row,intent) { return {schemaVersion:1,executionId:intent.executionId,incarnationId:row.incarnationId,attempt:intent.attempt}; }
|
|
39
|
+
function validateAdmittedAction(action,capability) {
|
|
40
|
+
if(action?.kind==='session'){
|
|
41
|
+
objectAt(action,['kind','name'],['kind','name']);
|
|
42
|
+
if(!['start','restart'].includes(action.name)||capability!==null)throw oatsError('invalid-declaration','invalid kernel session admission');
|
|
43
|
+
}else{
|
|
44
|
+
validateCapturedAction(action);
|
|
45
|
+
if(!isMaterializedCapabilityId(capability)||action.capability!==undefined&&action.capability!==capability)throw oatsError('invalid-declaration','invalid intent capability owner');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function validateIntent(intent) {
|
|
49
|
+
objectAt(intent,["executionId","capability","action","inputIntegrity","state","attempt","receipt","replayable"],["executionId","capability","action","inputIntegrity","state","attempt","receipt","replayable"]);
|
|
50
|
+
if(typeof intent.replayable!=="boolean")throw oatsError("invalid-declaration","invalid intent replay evidence");
|
|
51
|
+
validateAdmittedAction(intent.action,intent.capability);
|
|
52
|
+
stringAt(intent.executionId,"/executionId",{pattern:/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/});
|
|
53
|
+
if (!["command","operation","hook","session"].includes(intent.action.kind) || !INTENT_STATES.has(intent.state)) throw oatsError("invalid-declaration","invalid admitted action/state");
|
|
54
|
+
if (!Number.isSafeInteger(intent.attempt) || intent.attempt < 1) throw oatsError("invalid-declaration","invalid intent attempt");
|
|
55
|
+
objectAt(intent.inputIntegrity,["format","value"],["format","value"]);
|
|
56
|
+
if (intent.inputIntegrity.format !== "oats.json.v1" || !/^sha256-[a-f0-9]{64}$/.test(intent.inputIntegrity.value)) throw oatsError("invalid-declaration","invalid intent input integrity");
|
|
57
|
+
canonicalJson(intent.receipt,RECEIPT_LIMITS);
|
|
58
|
+
}
|
|
59
|
+
export function validateCapturedInstanceReference(row) {
|
|
60
|
+
objectAt(row,["home","instance","agent","kind","status","executionBinding","responsibleHuman","incarnationId","custody","intents","custodyFailure","nativeScaffold"],
|
|
61
|
+
["home","instance","agent","kind","status","executionBinding","responsibleHuman","incarnationId","custody","intents"]);
|
|
62
|
+
for(const key of ["home","instance","agent"]) stringAt(row[key],`/${key}`);
|
|
63
|
+
if(!isAbsolute(row.home)||resolve(row.home)!==row.home) throw oatsError("invalid-declaration","captured home must be normalized absolute");
|
|
64
|
+
if(!['persistent','helper'].includes(row.kind)||!STATUSES.has(row.status)) throw oatsError("invalid-declaration","invalid captured instance kind/status");
|
|
65
|
+
validateIncarnationId(row.incarnationId);validateExecutionBinding(row.executionBinding);human(row.responsibleHuman);
|
|
66
|
+
if(row.nativeScaffold!==undefined){
|
|
67
|
+
objectAt(row.nativeScaffold,['schemaVersion','directories'],['schemaVersion','directories']);
|
|
68
|
+
if(row.nativeScaffold.schemaVersion!==1)throw oatsError('invalid-declaration','invalid native scaffold witness');
|
|
69
|
+
const names=['historyRoot','history','retirementRoot','baselines'];objectAt(row.nativeScaffold.directories,names,names);
|
|
70
|
+
for(const value of Object.values(row.nativeScaffold.directories))directoryIdentity(value);
|
|
71
|
+
}
|
|
72
|
+
if(row.custodyFailure!==undefined){
|
|
73
|
+
objectAt(row.custodyFailure,["code","message"],["code","message"]);
|
|
74
|
+
for(const key of ["code","message"]){stringAt(row.custodyFailure[key],`/custodyFailure/${key}`);if(row.custodyFailure[key].length>200)throw oatsError("resource-limit","custody diagnostic exceeds bound");}
|
|
75
|
+
}
|
|
76
|
+
objectAt(row.custody,["home","work"],["home","work"]);directoryIdentity(row.custody.home);directoryIdentity(row.custody.work);
|
|
77
|
+
if(!Array.isArray(row.intents)||row.intents.length>128) throw oatsError("resource-limit","captured instance intent retention limit reached");
|
|
78
|
+
const ids=new Set();let pending=0;
|
|
79
|
+
for(const intent of row.intents){validateIntent(intent);if(ids.has(intent.executionId))throw oatsError("invalid-declaration","duplicate intent ID");ids.add(intent.executionId);if(intent.state!=="completed")pending++;}
|
|
80
|
+
if(pending>1) throw oatsError("invalid-declaration","multiple unsettled captured intents");
|
|
81
|
+
return row;
|
|
82
|
+
}
|
|
83
|
+
function validateIndex(value) {
|
|
84
|
+
canonicalJson(value,LIMITS);objectAt(value,["schemaVersion","instances"],["schemaVersion","instances"]);
|
|
85
|
+
if(value.schemaVersion!==2) throw oatsError("migration-required","captured instance index lacks incarnation/admission evidence; no automatic backfill");
|
|
86
|
+
if(!Array.isArray(value.instances)) throw oatsError("invalid-declaration","invalid captured instance index");
|
|
87
|
+
const ids=new Set();let prior;
|
|
88
|
+
for(const row of value.instances){validateCapturedInstanceReference(row);if(ids.has(row.incarnationId)||(prior!==undefined&&compareUtf8(prior,row.home)>=0))throw oatsError("invalid-declaration","duplicate/unordered captured instance references");ids.add(row.incarnationId);prior=row.home;}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
export function readCapturedInstanceIndex(deployment) {
|
|
92
|
+
const root=portableStateDirectory(deployment,false);
|
|
93
|
+
if(!root)return {schemaVersion:2,instances:[]};
|
|
94
|
+
const bytes=readPortableBytes(join(root,FILE),{allowMissing:true});
|
|
95
|
+
return bytes===null?{schemaVersion:2,instances:[]}:validateIndex(parseStrictJson(bytes,LIMITS));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Read and verify current owned metadata; old/unwitnessed homes are evidence,
|
|
99
|
+
* never newly minted identities. Recheck roots after the descriptor read. */
|
|
100
|
+
export function readCapturedInstanceMetadata(home) {
|
|
101
|
+
if(typeof home!=="string"||!isAbsolute(home))throw oatsError("invalid-declaration","captured home must be explicit and absolute");
|
|
102
|
+
const actual=portableScope(home),homeIdentity=capturedDirectoryIdentity(home),workIdentity=capturedDirectoryIdentity(join(home,"work"));
|
|
103
|
+
const metadata=parseStrictJson(readPortableBytes(join(actual,"instance.json")),LIMITS);
|
|
104
|
+
if(!metadata.incarnationId||!metadata.captured?.custody)throw oatsError("migration-required","captured home lacks incarnation custody; explicit reprovisioning required without deleting prior data");
|
|
105
|
+
validateIncarnationId(metadata.incarnationId);validateExecutionBinding(metadata.executionBinding);
|
|
106
|
+
if(typeof metadata.home!=="string"||portableScope(metadata.home)!==actual||metadata.work!=="directory"
|
|
107
|
+
||!same(metadata.captured.custody,{home:homeIdentity,work:workIdentity})
|
|
108
|
+
||!same(capturedDirectoryIdentity(home),homeIdentity)||!same(capturedDirectoryIdentity(join(home,"work")),workIdentity))throw oatsError("integrity-drift","captured home/work ownership changed");
|
|
109
|
+
return metadata;
|
|
110
|
+
}
|
|
111
|
+
function matchedRow(index,home) {
|
|
112
|
+
const metadata=readCapturedInstanceMetadata(home),actual=portableScope(home),row=index.instances.find(item=>item.home===actual);
|
|
113
|
+
if(!row)throw oatsError("invalid-resolution","captured incarnation is not indexed");
|
|
114
|
+
if(row.incarnationId!==metadata.incarnationId||row.instance!==metadata.instance||row.agent!==metadata.agent||row.kind!==metadata.kind
|
|
115
|
+
||!same(row.custody,metadata.captured.custody)||!same(row.executionBinding,metadata.executionBinding)||!same(row.responsibleHuman,metadata.responsibleHuman))throw oatsError("invalid-resolution","captured index differs from current incarnation metadata");
|
|
116
|
+
return {row,metadata};
|
|
117
|
+
}
|
|
118
|
+
export function readCapturedInstanceAuthority(deployment,home) {
|
|
119
|
+
return matchedRow(readCapturedInstanceIndex(deployment),home);
|
|
120
|
+
}
|
|
121
|
+
const AUTHORITY_FIELDS = ["home","instance","agent","kind","executionBinding","responsibleHuman","incarnationId","custody"];
|
|
122
|
+
function sameAuthority(a,b){return AUTHORITY_FIELDS.every(key=>same(a[key],b[key]))&&same(a.nativeScaffold??null,b.nativeScaffold??null);}
|
|
123
|
+
export function assertCapturedInstanceCustody(original) {
|
|
124
|
+
for(const [key,path] of [["home",original.home],["work",join(original.home,"work")]]){
|
|
125
|
+
if(!same(capturedDirectoryIdentity(path),original.custody[key]))throw oatsError("integrity-drift","captured home/work changed after preflight");
|
|
126
|
+
}
|
|
127
|
+
const {row}=readCapturedInstanceAuthority(original.executionBinding.deployment,original.home);
|
|
128
|
+
if(!sameAuthority(original,row))throw oatsError("integrity-drift","captured instance authority changed after preflight");
|
|
129
|
+
return row;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Failure reporting deliberately does NOT read/resolve the home: it may now
|
|
133
|
+
* belong to someone else. Match the original indexed authority and attempt,
|
|
134
|
+
* preserve receipts there, and never use this path to certify success. */
|
|
135
|
+
export function recordCapturedCustodyFailure(original,{meta={},intents={},code="integrity-drift",message="captured home/work custody changed"}={}) {
|
|
136
|
+
validateCapturedInstanceReference(original);objectAt(meta,null,[]);objectAt(intents,null,[]);
|
|
137
|
+
return withPortableStateWrite(original.executionBinding.deployment,context=>{
|
|
138
|
+
const index=readCapturedInstanceIndex(context.deployment),row=index.instances.find(item=>item.incarnationId===original.incarnationId);
|
|
139
|
+
if(!row||!sameAuthority(original,row))throw oatsError("selection-changed","original indexed custody no longer matches failure report");
|
|
140
|
+
// This reporter owns only the activation transition it acquired before
|
|
141
|
+
// hooks. Check it before changing ANY receipt/status, even on failure.
|
|
142
|
+
if(row.status!=="spawn-hooks-running")throw oatsError("selection-changed","activation no longer owns the indexed lifecycle state");
|
|
143
|
+
for(const [capability,ref] of Object.entries(intents)){
|
|
144
|
+
const intent=matchedIntent(row,ref);
|
|
145
|
+
if(intent.capability!==capability)throw oatsError("invalid-resolution","observed receipt owner differs from admitted capability");
|
|
146
|
+
if(Object.hasOwn(meta,capability)){canonicalJson(meta[capability],RECEIPT_LIMITS);intent.receipt=meta[capability];}
|
|
147
|
+
if(intent.state==="running"||intent.state==="admitted"){intent.state="unconfirmed";intent.replayable=false;}
|
|
148
|
+
}
|
|
149
|
+
for(const capability of Object.keys(meta))if(!Object.hasOwn(intents,capability))throw oatsError("invalid-resolution","observed receipt lacks its admitted intent");
|
|
150
|
+
row.status="spawn-failed-cleanup-required";
|
|
151
|
+
row.custodyFailure={code:String(code).slice(0,200)||"integrity-drift",message:String(message).slice(0,200)||"custody failure"};
|
|
152
|
+
save(context,index);return {home:row.home,incarnationId:row.incarnationId,executionBinding:row.executionBinding,custodyFailure:row.custodyFailure};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Sync only already-owned metadata paths. If synchronization fails after
|
|
156
|
+
* publication, callers retain custody rather than claim admission completed. */
|
|
157
|
+
export function syncCapturedMetadata(path) {
|
|
158
|
+
const stat=lstatSync(path);
|
|
159
|
+
if(!stat.isFile()&&!stat.isDirectory())throw oatsError("integrity-drift","captured metadata sync target changed");
|
|
160
|
+
const fd=openSync(path,constants.O_RDONLY|(constants.O_NOFOLLOW??0));
|
|
161
|
+
try{fsyncSync(fd);}finally{closeSync(fd);}
|
|
162
|
+
}
|
|
163
|
+
function save(context,index){
|
|
164
|
+
validateIndex(index);const file=join(context.root,FILE);
|
|
165
|
+
writeGuardedPortableDocument(context,file,index);
|
|
166
|
+
for(const path of [file,context.root,dirname(context.root),context.deployment])syncCapturedMetadata(path);
|
|
167
|
+
}
|
|
168
|
+
export function registerCapturedInstance(deployment,home) {
|
|
169
|
+
return withPortableStateWrite(deployment,context=>{
|
|
170
|
+
const index=readCapturedInstanceIndex(context.deployment),metadata=readCapturedInstanceMetadata(home),actual=portableScope(home);
|
|
171
|
+
if(index.instances.some(row=>row.home===actual))throw oatsError("selection-changed","captured home has retained lifecycle obligations; no replacement/backfill");
|
|
172
|
+
if(portableScope(metadata.executionBinding.deployment)!==context.deployment)throw oatsError("invalid-resolution","incarnation deployment mismatch");
|
|
173
|
+
const row={home:actual,instance:metadata.instance,agent:metadata.agent,kind:metadata.kind,status:"scaffolded-hooks-pending",executionBinding:metadata.executionBinding,
|
|
174
|
+
responsibleHuman:metadata.responsibleHuman,incarnationId:metadata.incarnationId,custody:metadata.captured.custody,intents:[]};
|
|
175
|
+
validateCapturedInstanceReference(row);index.instances.push(row);index.instances.sort((a,b)=>compareUtf8(a.home,b.home));save(context,index);return row;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
export function markCapturedNativeScaffold(deployment,home,proof) {
|
|
179
|
+
return withPortableStateWrite(deployment,context=>{
|
|
180
|
+
const index=readCapturedInstanceIndex(context.deployment),{row,metadata}=matchedRow(index,home);
|
|
181
|
+
if(row.nativeScaffold||row.status!=='scaffolded-hooks-pending'||row.intents.length)throw oatsError('selection-changed','native scaffold cannot be backfilled after admission');
|
|
182
|
+
if(!same(metadata.captured.nativeScaffold,proof))throw oatsError('invalid-resolution','native scaffold proof differs from owned metadata');
|
|
183
|
+
row.nativeScaffold=proof;save(context,index);return row;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export function setCapturedInstanceStatus(deployment,home,status,{expectedStatus}={}) {
|
|
187
|
+
if(!STATUSES.has(status))throw oatsError("invalid-declaration","invalid captured lifecycle status");
|
|
188
|
+
return withPortableStateWrite(deployment,context=>{
|
|
189
|
+
const index=readCapturedInstanceIndex(context.deployment),{row}=matchedRow(index,home);
|
|
190
|
+
if(expectedStatus!==undefined&&row.status!==expectedStatus)throw oatsError("selection-changed","captured lifecycle status changed");
|
|
191
|
+
row.status=status;save(context,index);return row;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** New intent or EXPLICIT retry; input commits only bounded nonsecret request
|
|
196
|
+
* data, never runtime environment or credential values. */
|
|
197
|
+
export function admitCapturedInstanceAction({deployment,home,executionBinding,capability,action,input={},retryExecutionId,expectedStatus}) {
|
|
198
|
+
validateExecutionBinding(executionBinding);
|
|
199
|
+
if(expectedStatus!==undefined&&!STATUSES.has(expectedStatus))throw oatsError('invalid-declaration','invalid admission source state');
|
|
200
|
+
validateAdmittedAction(action,capability);canonicalJson(input,{maxBytes:128*1024,maxDepth:24,maxEntries:8192});
|
|
201
|
+
const inputIntegrity=jsonIntegrity(input);
|
|
202
|
+
return withPortableStateWrite(deployment,context=>{
|
|
203
|
+
const index=readCapturedInstanceIndex(context.deployment),{row,metadata}=matchedRow(index,home);
|
|
204
|
+
if(!same(row.executionBinding,executionBinding))throw oatsError("invalid-resolution","admission selection differs from current indexed incarnation");
|
|
205
|
+
if(expectedStatus!==undefined&&(row.status!==expectedStatus||metadata.captured.lifecycle!==expectedStatus)){
|
|
206
|
+
const error=oatsError('selection-changed','native admission no longer owns matching source lifecycle');error.home=row.home;
|
|
207
|
+
error.capturedCustody={incarnationId:row.incarnationId,executionBinding:row.executionBinding,indexedStatus:row.status,metadataStatus:metadata.captured.lifecycle,expectedStatus,held:true};
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
let intent;
|
|
211
|
+
if(retryExecutionId!==undefined){
|
|
212
|
+
intent=row.intents.find(item=>item.executionId===retryExecutionId);
|
|
213
|
+
if(!intent||intent.capability!==capability||!same(intent.action,action)||!same(intent.inputIntegrity,inputIntegrity))throw oatsError("invalid-resolution","retry does not match the admitted action/input");
|
|
214
|
+
if(intent.state==="completed")return {intent:intentRef(row,intent),replayed:true,replayable:intent.replayable,receipt:intent.receipt};
|
|
215
|
+
if(!["unconfirmed","blocked"].includes(intent.state))throw oatsError("selection-changed","intent is admitted/running; explicit reconciliation is required before retry");
|
|
216
|
+
intent.attempt++;intent.state="admitted";
|
|
217
|
+
}else{
|
|
218
|
+
if(row.intents.some(item=>item.state!=="completed"))throw oatsError("selection-changed","an unsettled captured intent must be reconciled/retried, not replaced");
|
|
219
|
+
if(row.intents.length>=128)throw oatsError("resource-limit","captured intent history is full; no automatic pruning");
|
|
220
|
+
const previous=row.intents.findLast(item=>item.capability===capability);
|
|
221
|
+
const receipt=previous?previous.receipt:capability!==null&&metadata.capabilityMeta&&Object.hasOwn(metadata.capabilityMeta,capability)?metadata.capabilityMeta[capability]:null;
|
|
222
|
+
canonicalJson(receipt,RECEIPT_LIMITS);
|
|
223
|
+
intent={executionId:randomUUID(),capability,action,inputIntegrity,state:"admitted",attempt:1,receipt,replayable:false};row.intents.push(intent);
|
|
224
|
+
}
|
|
225
|
+
save(context,index);return {intent:intentRef(row,intent),replayed:false,receipt:intent.receipt};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function matchedIntent(row,ref,action) {
|
|
229
|
+
validateIntentRef(ref);const intent=row.intents.find(item=>item.executionId===ref.executionId);
|
|
230
|
+
if(row.incarnationId!==ref.incarnationId||!intent||intent.attempt!==ref.attempt||(action&&!same(intent.action,action)))throw oatsError("invalid-resolution","intent reference differs from indexed incarnation/action/attempt");
|
|
231
|
+
return intent;
|
|
232
|
+
}
|
|
233
|
+
export function readCapturedIntent({deployment,home,intent:ref,action}) {
|
|
234
|
+
const {row}=matchedRow(readCapturedInstanceIndex(deployment),home),intent=matchedIntent(row,ref,action);
|
|
235
|
+
return {...intent,incarnationId:row.incarnationId};
|
|
236
|
+
}
|
|
237
|
+
export function failCapturedNativeIntent(original,ref,receipt) {
|
|
238
|
+
canonicalJson(receipt,RECEIPT_LIMITS);
|
|
239
|
+
return withPortableStateWrite(original.executionBinding.deployment,context=>{
|
|
240
|
+
const index=readCapturedInstanceIndex(context.deployment),row=index.instances.find(item=>item.incarnationId===original.incarnationId);
|
|
241
|
+
if(!row||!sameAuthority(original,row))throw oatsError('selection-changed','native failure lost original indexed authority');
|
|
242
|
+
if(row.status!=='start-running')throw oatsError('selection-changed','native failure no longer owns lifecycle state');
|
|
243
|
+
const intent=matchedIntent(row,ref);
|
|
244
|
+
if(intent.action.kind!=='session'||intent.capability!==null)throw oatsError('invalid-resolution','native failure is not its session intent');
|
|
245
|
+
if(intent.state!=='completed'){intent.state='unconfirmed';intent.receipt=receipt;intent.replayable=false;}
|
|
246
|
+
row.status='start-failed-cleanup-required';save(context,index);return intentRef(row,intent);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** An allocated native target is retained under the running logical intent
|
|
250
|
+
* BEFORE launch. It is an observation, not completion or a new identity. */
|
|
251
|
+
export function observeCapturedNativeTarget(original,ref,receipt) {
|
|
252
|
+
validateCapturedInstanceReference(original);canonicalJson(receipt,RECEIPT_LIMITS);
|
|
253
|
+
return withPortableStateWrite(original.executionBinding.deployment,context=>{
|
|
254
|
+
const index=readCapturedInstanceIndex(context.deployment),{row}=matchedRow(index,original.home);
|
|
255
|
+
if(!sameAuthority(original,row)||row.status!=='start-running')throw oatsError('selection-changed','native target observation lost original lifecycle custody');
|
|
256
|
+
const intent=matchedIntent(row,ref);
|
|
257
|
+
if(intent.capability!==null||intent.action.kind!=='session'||intent.state!=='running')throw oatsError('selection-changed','native target observation needs the running admitted session');
|
|
258
|
+
intent.receipt=receipt;intent.replayable=false;save(context,index);return intentRef(row,intent);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
export function beginCapturedIntent({deployment,home,intent:ref,action}) {
|
|
262
|
+
return withPortableStateWrite(deployment,context=>{
|
|
263
|
+
const index=readCapturedInstanceIndex(context.deployment),{row}=matchedRow(index,home),intent=matchedIntent(row,ref,action);
|
|
264
|
+
if(intent.state!=="admitted")throw oatsError("selection-changed","captured intent has already begun or settled");
|
|
265
|
+
intent.state="running";save(context,index);return intentRef(row,intent);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export function settleCapturedIntent({deployment,home,intent:ref,action,state,receipt,replayable=false}) {
|
|
269
|
+
if(typeof replayable!=="boolean")throw oatsError("invalid-declaration","invalid intent replay evidence");
|
|
270
|
+
if(!["completed","unconfirmed","blocked"].includes(state))throw oatsError("invalid-declaration","invalid captured intent outcome");
|
|
271
|
+
canonicalJson(receipt,RECEIPT_LIMITS);
|
|
272
|
+
return withPortableStateWrite(deployment,context=>{
|
|
273
|
+
const index=readCapturedInstanceIndex(context.deployment),{row}=matchedRow(index,home),intent=matchedIntent(row,ref,action);
|
|
274
|
+
if((state==="blocked"&&intent.state!=="admitted")||(state!=="blocked"&&intent.state!=="running"))throw oatsError("selection-changed","intent outcome does not match its admitted/running attempt");
|
|
275
|
+
intent.state=state;intent.receipt=receipt;intent.replayable=state==="completed"&&replayable;save(context,index);return intentRef(row,intent);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Provider-neutral captured action/lifecycle context. Provider bindings remain in
|
|
2
|
+
* OATS_BINDING_FILE; this companion supplies record/target intent and prior
|
|
3
|
+
* opaque provider receipt without provider-specific kernel fields. */
|
|
4
|
+
import { lstatSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { readCapturedInstanceAuthority, readCapturedIntent } from "./captured-instance-index.mjs";
|
|
6
|
+
import { validateIncarnationId, validateIntentRef } from "./captured-admission-shape.mjs";
|
|
7
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { canonicalJson } from "./portable-values.mjs";
|
|
9
|
+
import { objectAt, stringAt } from "./portable-shape.mjs";
|
|
10
|
+
import { portableScope, portableStateDirectory } from "./portable-state.mjs";
|
|
11
|
+
import { validateExecutionBinding } from "./schedule-capsule.mjs";
|
|
12
|
+
import { validateCapturedSubject, validateCapturedContext, validateMessagingChoice } from "./resolution-shape.mjs";
|
|
13
|
+
import { validateCapturedAction } from "./captured-action-shape.mjs";
|
|
14
|
+
import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
|
|
15
|
+
import { oatsError } from "./errors.mjs";
|
|
16
|
+
|
|
17
|
+
export const CAPTURED_INVOCATION_CONTEXT_LIMITS = Object.freeze({ maxBytes: 512 * 1024, maxDepth: 32, maxEntries: 16_384 });
|
|
18
|
+
export const CAPTURED_PRIOR_RECEIPT_LIMITS = Object.freeze({ maxBytes: 128 * 1024, maxDepth: 24, maxEntries: 8192 });
|
|
19
|
+
const FIELDS = ["schemaVersion", "executionBinding", "subject", "instance", "context", "responsibleHuman", "messagingChoice", "capability", "action", "priorReceipt", "intent"];
|
|
20
|
+
|
|
21
|
+
function human(value, pointer) {
|
|
22
|
+
if (value === null) return;
|
|
23
|
+
objectAt(value, ["provider", "id"], ["provider", "id"], pointer);
|
|
24
|
+
stringAt(value.provider, `${pointer}/provider`); stringAt(value.id, `${pointer}/id`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validateCapturedInvocationContext(value) {
|
|
28
|
+
canonicalJson(value, CAPTURED_INVOCATION_CONTEXT_LIMITS); objectAt(value, FIELDS, FIELDS);
|
|
29
|
+
if (value.schemaVersion !== 1) throw oatsError("unsupported-wire-version", "captured invocation context schemaVersion must be 1");
|
|
30
|
+
validateExecutionBinding(value.executionBinding);
|
|
31
|
+
validateCapturedSubject(value.subject);
|
|
32
|
+
const subjectName = value.subject.kind === "persistent" ? value.subject.soul.alias : value.subject.name;
|
|
33
|
+
if (value.instance !== null) {
|
|
34
|
+
objectAt(value.instance, ["home", "work", "name", "agent", "incarnationId"], ["home", "work", "name", "agent", "incarnationId"], "/instance");
|
|
35
|
+
validateIncarnationId(value.instance.incarnationId);
|
|
36
|
+
for (const key of ["home", "work"]) {
|
|
37
|
+
stringAt(value.instance[key], `/instance/${key}`);
|
|
38
|
+
if (!isAbsolute(value.instance[key]) || resolve(value.instance[key]) !== value.instance[key] || value.instance[key].includes("\0")) throw oatsError("invalid-declaration", `captured invocation ${key} must be normalized absolute`);
|
|
39
|
+
}
|
|
40
|
+
stringAt(value.instance.name, "/instance/name"); stringAt(value.instance.agent, "/instance/agent");
|
|
41
|
+
if (value.instance.agent !== subjectName || value.instance.work !== join(value.instance.home, "work")) throw oatsError("invalid-resolution", "captured invocation target differs from its subject/home");
|
|
42
|
+
}
|
|
43
|
+
validateCapturedContext(value.context);
|
|
44
|
+
human(value.responsibleHuman, "/responsibleHuman");
|
|
45
|
+
// Reuse the existing messaging-choice codec, not another privacy/policy model.
|
|
46
|
+
const messagingProvider = value.messagingChoice?.enabled === true ? value.messagingChoice.privateKey?.provider : undefined;
|
|
47
|
+
if (messagingProvider !== undefined && !isMaterializedCapabilityId(messagingProvider)) throw oatsError("invalid-declaration", "invalid invocation messaging provider");
|
|
48
|
+
validateMessagingChoice(value.messagingChoice, { context: value.context,
|
|
49
|
+
bindings: messagingProvider === undefined ? {} : { messaging: { capability: messagingProvider } } });
|
|
50
|
+
const expectedHuman = value.messagingChoice.enabled ? value.messagingChoice.privateKey.human : null;
|
|
51
|
+
if (canonicalJson(value.responsibleHuman) !== canonicalJson(expectedHuman)) throw oatsError("invalid-resolution", "invocation responsible human differs from captured messaging choice");
|
|
52
|
+
if (!isMaterializedCapabilityId(value.capability)) throw oatsError("invalid-declaration", "invalid invocation capability");
|
|
53
|
+
validateCapturedAction(value.action);
|
|
54
|
+
if (value.action.capability !== undefined && value.action.capability !== value.capability) throw oatsError("invalid-resolution", "invocation action capability differs from its owner");
|
|
55
|
+
if (value.action.kind === "hook" && value.instance === null) throw oatsError("invalid-resolution", "captured instance hook requires instance facts");
|
|
56
|
+
if (value.intent !== null) {
|
|
57
|
+
validateIntentRef(value.intent);
|
|
58
|
+
if (value.instance === null || value.intent.incarnationId !== value.instance.incarnationId) throw oatsError("invalid-resolution", "intent belongs to a different incarnation");
|
|
59
|
+
}
|
|
60
|
+
canonicalJson(value.priorReceipt, CAPTURED_PRIOR_RECEIPT_LIMITS);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildCapturedInvocationContext(input) {
|
|
65
|
+
const { loaded, action, instance = null } = input;
|
|
66
|
+
if (!loaded?.record || !loaded?.resolution || !loaded?.capability) throw oatsError("invalid-resolution", "captured invocation context requires one verified action");
|
|
67
|
+
const { record } = loaded, subject = record.subject;
|
|
68
|
+
const subjectName = subject.kind === "persistent" ? subject.soul.alias : subject.name;
|
|
69
|
+
if (instance !== null && instance.agent !== subjectName) throw oatsError("invalid-resolution", "captured invocation target agent differs from retained subject");
|
|
70
|
+
const expectedHuman = record.messagingChoice.enabled ? record.messagingChoice.privateKey.human : null;
|
|
71
|
+
let priorReceipt = null, target = null;
|
|
72
|
+
const intent = input.intent ?? null;
|
|
73
|
+
if (instance !== null) {
|
|
74
|
+
canonicalJson(instance);
|
|
75
|
+
objectAt(instance, ["home", "work", "name", "agent"], ["home", "work", "name", "agent"]);
|
|
76
|
+
if (typeof instance.home !== "string" || !isAbsolute(instance.home) || instance.work !== join(instance.home, "work")) throw oatsError("invalid-resolution", "captured invocation needs an explicit home/work target");
|
|
77
|
+
const home = portableScope(instance.home);
|
|
78
|
+
if (!lstatSync(instance.home).isDirectory()) throw oatsError("invalid-resolution", "captured invocation home must not be a symlink");
|
|
79
|
+
const { metadata, row } = readCapturedInstanceAuthority(loaded.deployment, home);
|
|
80
|
+
target = { ...instance, incarnationId: metadata.incarnationId };
|
|
81
|
+
validateExecutionBinding(metadata.executionBinding);
|
|
82
|
+
if (typeof metadata.home !== "string" || portableScope(metadata.home) !== home || metadata.instance !== instance.name
|
|
83
|
+
|| metadata.agent !== instance.agent || metadata.kind !== subject.kind
|
|
84
|
+
|| portableScope(metadata.executionBinding.deployment) !== portableScope(loaded.deployment)
|
|
85
|
+
|| canonicalJson(metadata.executionBinding.resolution) !== canonicalJson(loaded.resolution)
|
|
86
|
+
|| canonicalJson(metadata.responsibleHuman) !== canonicalJson(expectedHuman)) throw oatsError("invalid-resolution", "invocation target differs from captured instance metadata/record");
|
|
87
|
+
if (metadata.capabilityMeta !== undefined) objectAt(metadata.capabilityMeta, null, []);
|
|
88
|
+
const previous = row.intents.findLast(entry => entry.capability === loaded.capability.id);
|
|
89
|
+
priorReceipt = previous ? previous.receipt : metadata.capabilityMeta && Object.hasOwn(metadata.capabilityMeta, loaded.capability.id) ? metadata.capabilityMeta[loaded.capability.id] : null;
|
|
90
|
+
}
|
|
91
|
+
if (intent !== null) {
|
|
92
|
+
if (!target) throw oatsError("invalid-resolution", "admitted action requires an owned instance");
|
|
93
|
+
const admitted = readCapturedIntent({ deployment: loaded.deployment, home: instance.home, intent, action });
|
|
94
|
+
if (admitted.capability !== loaded.capability.id || !["admitted", "running"].includes(admitted.state)) throw oatsError("invalid-resolution", "invocation does not match an active admitted capability action");
|
|
95
|
+
priorReceipt = admitted.receipt;
|
|
96
|
+
}
|
|
97
|
+
if (Object.hasOwn(input, "priorReceipt") && canonicalJson(input.priorReceipt) !== canonicalJson(priorReceipt)) throw oatsError("invalid-resolution", "invocation prior receipt differs from stored provider receipt");
|
|
98
|
+
const value = { schemaVersion: 1,
|
|
99
|
+
executionBinding: { schemaVersion: 1, deployment: portableScope(loaded.deployment), resolution: loaded.resolution },
|
|
100
|
+
subject, instance: target, intent, context: record.context, responsibleHuman: expectedHuman,
|
|
101
|
+
messagingChoice: record.messagingChoice, capability: loaded.capability.id, action, priorReceipt };
|
|
102
|
+
return validateCapturedInvocationContext(structuredClone(value));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function withCapturedInvocationContextFile(invocation, run) {
|
|
106
|
+
validateCapturedInvocationContext(invocation);
|
|
107
|
+
if (typeof run !== "function") throw oatsError("invalid-declaration", "captured invocation context consumer must be a function");
|
|
108
|
+
const directory = mkdtempSync(join(portableStateDirectory(invocation.executionBinding.deployment, true), ".invocation-context-"));
|
|
109
|
+
const owned = lstatSync(directory), file = join(directory, "context.json"); let primary, result, completed = false;
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(file, canonicalJson(invocation, CAPTURED_INVOCATION_CONTEXT_LIMITS), { flag: "wx", mode: 0o600 });
|
|
112
|
+
result = run({ OATS_INVOCATION_CONTEXT_FILE: file }); completed = true; return result;
|
|
113
|
+
} catch (error) { primary = error; throw error; }
|
|
114
|
+
finally {
|
|
115
|
+
try {
|
|
116
|
+
const current = lstatSync(directory);
|
|
117
|
+
if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("integrity-drift", "invocation context directory ownership changed");
|
|
118
|
+
rmSync(directory, { recursive: true });
|
|
119
|
+
} catch (cleanup) {
|
|
120
|
+
if (!primary) {
|
|
121
|
+
const error = new AggregateError([cleanup], "captured invocation context cleanup failed after invocation", { cause: cleanup });
|
|
122
|
+
error.code = cleanup.code; error.invocationCompleted = completed; error.invocationResult = result; throw error;
|
|
123
|
+
}
|
|
124
|
+
const error = new AggregateError([primary, cleanup], "captured invocation context and cleanup failed", { cause: primary });
|
|
125
|
+
error.code = primary.code;
|
|
126
|
+
if (primary.invocationCompleted) { error.invocationCompleted = true; error.invocationResult = primary.invocationResult; }
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Explicit portable launch input, not a current launch-config/model resolver.
|
|
2
|
+
* OATS-managed entrypoints are retained; external host tools are explicitly
|
|
3
|
+
* selected/preflighted, not forced into a new capability layer. */
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { canonicalJson } from './portable-values.mjs';
|
|
6
|
+
import { objectAt, stringAt } from './portable-shape.mjs';
|
|
7
|
+
import { isMaterializedCapabilityId } from './capability-provenance.mjs';
|
|
8
|
+
import { oatsError } from './errors.mjs';
|
|
9
|
+
import { PI_SDK_HOST, validatePiHostRecipe } from './pi-sdk-host.mjs';
|
|
10
|
+
|
|
11
|
+
export function validateCapturedLaunchRequest(value, validateConfig) {
|
|
12
|
+
canonicalJson(value,{maxBytes:128*1024,maxDepth:16,maxEntries:4096});
|
|
13
|
+
objectAt(value,['runtime','executable','args','env','model','yolo'],['runtime','executable','args','env','model','yolo']);
|
|
14
|
+
if(typeof value.executable==='string'){
|
|
15
|
+
stringAt(value.executable,'/launch/executable');
|
|
16
|
+
if(!isAbsolute(value.executable)||resolve(value.executable)!==value.executable||value.executable.includes('\0'))throw oatsError('invalid-declaration','host launch executable must be explicit normalized absolute text');
|
|
17
|
+
}else{
|
|
18
|
+
objectAt(value.executable,['capability','command'],['capability','command']);
|
|
19
|
+
if(!isMaterializedCapabilityId(value.executable.capability))throw oatsError('invalid-declaration','managed launch entrypoint must name a selected capability');
|
|
20
|
+
stringAt(value.executable.command,'/launch/executable/command');
|
|
21
|
+
}
|
|
22
|
+
stringAt(value.model,'/launch/model');
|
|
23
|
+
if(typeof value.yolo!=='boolean')throw oatsError('invalid-declaration','captured launch requires explicit yolo');
|
|
24
|
+
validateConfig('captured',{...value,executable:'captured-resource'},'explicit launch request');
|
|
25
|
+
if(value.executable===PI_SDK_HOST || value.args?.includes('--oats-pi-host'))validatePiHostRecipe(value);
|
|
26
|
+
// All kernel/provider invocation aliases remain kernel-owned on this new
|
|
27
|
+
// surface. The legacy launch-config validator has a narrower historical list.
|
|
28
|
+
for(const name of Object.keys(value.env))if(/^(OATS_|PI_AGENT_|OAS_)/.test(name)||name==='PI_AGENTS_ROOT')throw oatsError('invalid-declaration','launch environment cannot override captured authority');
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function compileCapturedLaunchRequest(request,{artifacts,manifests,settings,resources},kernel) {
|
|
33
|
+
if(request===undefined)return null;
|
|
34
|
+
validateCapturedLaunchRequest(request,kernel.validateLaunchConfig);
|
|
35
|
+
const required=kernel.runtimeRequirements(request.runtime,[...manifests].filter(([id])=>Object.hasOwn(artifacts.capabilities,id)).map(([id,manifest])=>({id,manifest,settings:settings[id]})));
|
|
36
|
+
if(required.length)throw oatsError('needs-configuration','runtime package requirements need retained runtime roots and a qualified loader; no ambient package discovery was used');
|
|
37
|
+
const base={version:1,runtime:request.runtime,args:request.args,env:request.env,model:request.model,yolo:request.yolo,
|
|
38
|
+
hooks:{launch:{},env:{},contributions:[],pending:true},prompt:{kind:'task-file',file:'TASK.md'}};
|
|
39
|
+
if(typeof request.executable==='string')return {...base,executable:request.executable,executableResolvedFrom:'explicit-host'};
|
|
40
|
+
const {capability:id,command}=request.executable,manifest=manifests.get(id);
|
|
41
|
+
if(!Object.hasOwn(artifacts.capabilities,id)||!manifest?.commands||!Object.hasOwn(manifest.commands,command))throw oatsError('capability-not-selected','launch entrypoint is not declared by a selected capability');
|
|
42
|
+
const key=`executable:${id}:command:${command}`;
|
|
43
|
+
if(!Object.hasOwn(resources,key)||resources[key].kind!=='file')throw oatsError('resolution-incomplete','launch entrypoint lacks a retained executable resource');
|
|
44
|
+
const [, ...declaredArgs]=manifest.commands[command].trim().split(/\s+/);
|
|
45
|
+
return {...base,executable:'captured-resource',executableResource:key,entrypoint:{capability:id,command},args:[...declaredArgs,...request.args]};
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Bounded runner for one already-verified captured provider operation. The
|
|
2
|
+
* caller owns receipt interpretation and invocation-snapshot cleanup. */
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { oatsError } from "./errors.mjs";
|
|
5
|
+
|
|
6
|
+
export const CAPTURED_OPERATION_TIMEOUT_MS = 4 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
export function runCapturedOperationProcess({ file, args = [], cwd, env, timeoutMs = CAPTURED_OPERATION_TIMEOUT_MS }) {
|
|
9
|
+
if (typeof file !== "string" || !file || !Array.isArray(args) || args.some((arg) => typeof arg !== "string")) throw oatsError("invalid-declaration", "captured operation executable and arguments must be text");
|
|
10
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > CAPTURED_OPERATION_TIMEOUT_MS) throw oatsError("invalid-declaration", `captured operation timeout must be 1-${CAPTURED_OPERATION_TIMEOUT_MS}ms`);
|
|
11
|
+
return spawnSync(process.execPath, [file, ...args], {
|
|
12
|
+
cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
13
|
+
maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs, killSignal: "SIGKILL",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Kernel side of the Pi record-witness seam. Record support is deliberately
|
|
2
|
+
* capability-checked before admission: older path-only readers cannot launch a
|
|
3
|
+
* protected host. The record-library implementation is separately owned. */
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import * as records from "../packages/record/lib/native-history.mjs";
|
|
6
|
+
import { oatsError } from "./errors.mjs";
|
|
7
|
+
|
|
8
|
+
export function capturedPiSessionDirectory(home) {
|
|
9
|
+
const history = records.nativeHistoryPath(home);
|
|
10
|
+
return join(dirname(history), `${basename(history)}.pi`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requireCapturedPiRecordSupport() {
|
|
14
|
+
if (records.CAPTURED_PI_RECORD_VERSION !== 2
|
|
15
|
+
|| ["inspectCapturedPiRoot", "prepareCapturedPiStart", "assertCapturedPiStart"].some(name => typeof records[name] !== "function")) {
|
|
16
|
+
throw oatsError("E_PI_HOST_RECORD_UNAVAILABLE", "captured Pi requires complete versioned native-root discovery/read/capture guards; path-only record support cannot launch it");
|
|
17
|
+
}
|
|
18
|
+
return records;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function inspectCapturedPiRoot(home, authority) {
|
|
22
|
+
return requireCapturedPiRecordSupport().inspectCapturedPiRoot(home, authority);
|
|
23
|
+
}
|
|
24
|
+
export function prepareCapturedPiStart(home, authority) {
|
|
25
|
+
return requireCapturedPiRecordSupport().prepareCapturedPiStart(home, authority);
|
|
26
|
+
}
|
|
27
|
+
export function assertCapturedPiStart(home, nativeRecordId, authority) {
|
|
28
|
+
return requireCapturedPiRecordSupport().assertCapturedPiStart(home, nativeRecordId, authority);
|
|
29
|
+
}
|