@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,87 @@
|
|
|
1
|
+
/** Materialize one already-verified captured composition into a new private home.
|
|
2
|
+
* Home placement is supplied by the lifecycle owner; this module never discovers
|
|
3
|
+
* config, souls, work targets, launch software or instance roots. */
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { capturedDirectoryIdentity, registerCapturedInstance, syncCapturedMetadata } from "./captured-instance-index.mjs";
|
|
8
|
+
import { copyTreeSafe } from "./artifact-tree.mjs";
|
|
9
|
+
import { canonicalJson } from "./portable-values.mjs";
|
|
10
|
+
import { portableScope } from "./portable-state.mjs";
|
|
11
|
+
import { oatsError } from "./errors.mjs";
|
|
12
|
+
|
|
13
|
+
const INSTANCE_RE = /^[a-z0-9][a-z0-9-]{0,127}$/;
|
|
14
|
+
|
|
15
|
+
function sameDirectory(path, witness) {
|
|
16
|
+
try {
|
|
17
|
+
const current = lstatSync(path);
|
|
18
|
+
return current.isDirectory() && current.dev === witness.dev && current.ino === witness.ino;
|
|
19
|
+
} catch { return false; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateCapturedDirectoryTarget(home, instance) {
|
|
23
|
+
if (typeof home !== "string" || !isAbsolute(home) || resolve(home) !== home || basename(home) !== instance || !INSTANCE_RE.test(instance)) {
|
|
24
|
+
throw oatsError("invalid-declaration", "captured scaffold needs a normalized absolute home whose basename is its instance name");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function materializeCapturedDirectoryScaffold({ home, instance, loaded }) {
|
|
29
|
+
validateCapturedDirectoryTarget(home, instance);
|
|
30
|
+
if (!loaded?.record || !loaded?.composition || !loaded?.resolution || typeof loaded.deployment !== "string") throw oatsError("invalid-resolution", "captured scaffold requires a verified composition load");
|
|
31
|
+
const { record, composition } = loaded;
|
|
32
|
+
if (record.dispatch.composition?.mode !== "directory") throw oatsError("needs-configuration", "captured scaffold currently requires retained directory work mode");
|
|
33
|
+
if (existsSync(home)) throw oatsError("E_INSTANCE_EXISTS", `instance home already exists: ${home}`);
|
|
34
|
+
const parent = dirname(home), parentStat = lstatSync(parent);
|
|
35
|
+
if (!parentStat.isDirectory() || realpathSync(parent) !== parent) throw oatsError("E_NO_CANONICAL_ROOT", "captured scaffold parent must be a physical directory");
|
|
36
|
+
const subject = record.subject, agent = subject.kind === "persistent" ? subject.soul.alias : subject.name;
|
|
37
|
+
const responsibleHuman = record.messagingChoice.enabled ? record.messagingChoice.privateKey.human : null;
|
|
38
|
+
const executionBinding = { schemaVersion: 1, deployment: portableScope(loaded.deployment), resolution: loaded.resolution };
|
|
39
|
+
const bodyFile = loaded.resources.get(record.dispatch.composition.body);
|
|
40
|
+
if (!bodyFile) throw oatsError("invalid-resolution", "captured scaffold body is absent from verified resources");
|
|
41
|
+
const sourceRoot = dirname(bodyFile);
|
|
42
|
+
const metadata = {
|
|
43
|
+
instance, agent, kind: subject.kind, home, work: "directory", launched: false, incarnationId: randomUUID(),
|
|
44
|
+
executionBinding, responsibleHuman,
|
|
45
|
+
captured: { schemaVersion: 1, resolution: loaded.resolution, lifecycle: "scaffolded-hooks-pending" },
|
|
46
|
+
capabilities: [...loaded.capabilities.values()].map(({ id, manifest, settings }) => ({ id, version: manifest.version, settings })),
|
|
47
|
+
};
|
|
48
|
+
let owned, primary, indexAttempted = false;
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(home, { mode: 0o700 }); owned = lstatSync(home);
|
|
51
|
+
symlinkSync(sourceRoot, join(home, "soul"));
|
|
52
|
+
writeFileSync(join(home, "AGENTS.md"), composition.text, { mode: 0o600 });
|
|
53
|
+
symlinkSync("AGENTS.md", join(home, "CLAUDE.md"));
|
|
54
|
+
mkdirSync(join(home, ".agents", "skills"), { recursive: true, mode: 0o700 });
|
|
55
|
+
mkdirSync(join(home, ".claude"), { mode: 0o700 });
|
|
56
|
+
for (const skill of composition.skills) copyTreeSafe(skill.path, join(home, ".agents", "skills", skill.name));
|
|
57
|
+
symlinkSync(join("..", ".agents", "skills"), join(home, ".claude", "skills"));
|
|
58
|
+
mkdirSync(join(home, "work"), { mode: 0o700 });
|
|
59
|
+
for (const skill of composition.skills) {
|
|
60
|
+
const file = join(home, ".agents", "skills", skill.name, "SKILL.md");
|
|
61
|
+
if (!existsSync(file) || !lstatSync(file).isFile()) throw oatsError("resolution-incomplete", `captured skill ${skill.name} did not materialize`);
|
|
62
|
+
}
|
|
63
|
+
metadata.captured.custody = { home: capturedDirectoryIdentity(home), work: capturedDirectoryIdentity(join(home, "work")) };
|
|
64
|
+
writeFileSync(join(home, "instance.json"), canonicalJson(metadata), { flag: "wx", mode: 0o600 });
|
|
65
|
+
for (const path of [join(home, "instance.json"), join(home, "work"), home, parent]) syncCapturedMetadata(path);
|
|
66
|
+
indexAttempted = true;
|
|
67
|
+
registerCapturedInstance(loaded.deployment, home);
|
|
68
|
+
return { home, instance, agent, work: "directory", incarnationId: metadata.incarnationId, executionBinding, responsibleHuman, launched: false, hooksPending: true };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
primary = error;
|
|
71
|
+
// Publication/guard cleanup may fail AFTER index publication. Retain the
|
|
72
|
+
// home rather than erase evidence just because the call did not return.
|
|
73
|
+
if (indexAttempted) { error.home = home; error.cleanupRequired = true; }
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (primary && owned && !indexAttempted) {
|
|
78
|
+
try {
|
|
79
|
+
if (!sameDirectory(home, owned)) throw oatsError("integrity-drift", "captured scaffold home ownership changed during rollback");
|
|
80
|
+
rmSync(home, { recursive: true });
|
|
81
|
+
} catch (cleanup) {
|
|
82
|
+
const error = new AggregateError([primary, cleanup], "captured scaffold and rollback failed", { cause: primary });
|
|
83
|
+
error.code = primary.code; throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Explicit independent-dispatch selectors, with the captured command's own
|
|
2
|
+
* environment as a propagation channel. Never infer a record from cwd/locks. */
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { validateResolutionRef } from "./resolution-shape.mjs";
|
|
5
|
+
import { oatsError } from "./errors.mjs";
|
|
6
|
+
|
|
7
|
+
export function capturedSelector(argv, env = process.env) {
|
|
8
|
+
const values = new Map(), args = [];
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === "--") { args.push(...argv.slice(i)); break; }
|
|
12
|
+
const match = /^(--deployment|--resolution|--artifact-set)(?:=(.*))?$/.exec(arg);
|
|
13
|
+
if (!match) { args.push(arg); continue; }
|
|
14
|
+
const name = match[1].slice(2), value = match[2] ?? argv[++i];
|
|
15
|
+
if (values.has(name) || !value || value.startsWith("--")) throw oatsError("E_BAD_ARGS", `--${name} needs one unambiguous value`);
|
|
16
|
+
values.set(name, value);
|
|
17
|
+
}
|
|
18
|
+
const explicit = values.size > 0;
|
|
19
|
+
const deployment = explicit ? values.get("deployment") : env.OATS_DEPLOYMENT;
|
|
20
|
+
if (values.has("resolution") && values.has("artifact-set")) throw oatsError("E_BAD_ARGS", "choose a resolution or an artifact set, never both");
|
|
21
|
+
const artifactSet = values.get("artifact-set");
|
|
22
|
+
const id = explicit ? artifactSet ?? values.get("resolution") : env.OATS_RESOLUTION;
|
|
23
|
+
if (!explicit && deployment === undefined && id === undefined) return null;
|
|
24
|
+
if (typeof deployment !== "string" || !isAbsolute(deployment) || typeof id !== "string") throw oatsError("E_BAD_ARGS", "portable selectors need --deployment <absolute path> and one --resolution or --artifact-set <sha256-id>");
|
|
25
|
+
const resolution = { schemaVersion: 1, id };
|
|
26
|
+
validateResolutionRef(resolution);
|
|
27
|
+
return { deployment, ...(artifactSet === undefined ? { resolution } : { artifactSet }), args, explicit };
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Closed inputs for the existing native session adapters. No config lookup,
|
|
2
|
+
* endpoint allocation, backend registry or runtime/provider readiness probe. */
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { canonicalJson } from './portable-values.mjs';
|
|
5
|
+
import { objectAt } from './portable-shape.mjs';
|
|
6
|
+
import { HERDR_SUPPORTED_PROTOCOLS, isSupportedHerdrProtocol, validHerdrTarget } from './herdr.mjs';
|
|
7
|
+
import { oatsError } from './errors.mjs';
|
|
8
|
+
|
|
9
|
+
const same = (a,b) => canonicalJson(a) === canonicalJson(b);
|
|
10
|
+
const absolute = value => typeof value === 'string' && isAbsolute(value) && resolve(value) === value && !value.includes('\0');
|
|
11
|
+
export function validateCapturedSessionBackend(value) {
|
|
12
|
+
canonicalJson(value);
|
|
13
|
+
const fields=value?.backend==='herdr'?['backend','binary','socket','protocol']:['backend','binary','socket','session'];
|
|
14
|
+
objectAt(value,fields,fields);
|
|
15
|
+
if(!['tmux','herdr'].includes(value.backend)||!absolute(value.binary)||!absolute(value.socket))throw oatsError('needs-configuration','captured native backend needs explicit normalized host binary/socket');
|
|
16
|
+
if(value.backend==='herdr'){
|
|
17
|
+
if(!isSupportedHerdrProtocol(value.protocol))throw oatsError('needs-configuration',`captured Herdr requires an explicitly selected protocol in ${HERDR_SUPPORTED_PROTOCOLS.join(', ')}`);
|
|
18
|
+
}else if(typeof value.session!=='string'||!/^[A-Za-z0-9_-]+$/.test(value.session))throw oatsError('needs-configuration','captured tmux needs an explicit supported session name');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateCapturedSessionTarget(value,backend,instance) {
|
|
23
|
+
validateCapturedSessionBackend(backend);canonicalJson(value);
|
|
24
|
+
if(backend.backend==='tmux'){
|
|
25
|
+
objectAt(value,['backend','session','window','socket'],['backend','session','window','socket']);
|
|
26
|
+
if(!same(value,{backend:'tmux',session:backend.session,window:instance,socket:backend.socket}))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured tmux target differs from admitted endpoint');
|
|
27
|
+
}else{
|
|
28
|
+
objectAt(value,['backend','binary','socket','protocol','workspaceId','paneId','terminalId'],['backend','binary','socket','protocol','workspaceId','paneId','terminalId']);
|
|
29
|
+
if(!validHerdrTarget(value)||value.binary!==backend.binary||value.socket!==backend.socket||value.protocol!==backend.protocol
|
|
30
|
+
||[value.workspaceId,value.paneId,value.terminalId].some(id=>id.includes('\0')))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured Herdr target differs from admitted endpoint/identifier contract');
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** First placement admits a server endpoint, not caller-chosen Herdr IDs.
|
|
36
|
+
* Subsequent IDs must come from independently retained native observations. */
|
|
37
|
+
export function assertCapturedSessionPlacement(metadata,backend,knownTargets=[]) {
|
|
38
|
+
validateCapturedSessionBackend(backend);
|
|
39
|
+
if(typeof metadata.launched!=='boolean')throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured metadata lacks native launch state');
|
|
40
|
+
if(metadata.launched&&!Object.hasOwn(metadata,backend.backend==='herdr'?'sessionTarget':'tmux'))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','launched metadata lacks its native target');
|
|
41
|
+
if(Object.hasOwn(metadata,'backend')&&metadata.backend!==backend.backend)throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','captured metadata selects a different backend');
|
|
42
|
+
if(backend.backend==='tmux'){
|
|
43
|
+
if(Object.hasOwn(metadata,'sessionTarget'))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','Herdr target cannot redirect captured tmux');
|
|
44
|
+
if(Object.hasOwn(metadata,'tmux'))validateCapturedSessionTarget({backend:'tmux',...metadata.tmux},backend,metadata.instance);
|
|
45
|
+
}else{
|
|
46
|
+
if(Object.hasOwn(metadata,'tmux'))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','tmux metadata cannot redirect captured Herdr');
|
|
47
|
+
if(Object.hasOwn(metadata,'sessionTarget')){
|
|
48
|
+
validateCapturedSessionTarget(metadata.sessionTarget,backend,metadata.instance);
|
|
49
|
+
if(!knownTargets.some(target=>same(target,metadata.sessionTarget)))throw oatsError('E_RUNTIME_AUTHORITY_MISMATCH','Herdr metadata target lacks independent indexed custody');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Private captured-source registration input. This is a synchronous hook
|
|
2
|
+
* snapshot, never durable worker authority or an ambient configuration file. */
|
|
3
|
+
import { lstatSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { validateSoulIdentity } from "./portable-identity.mjs";
|
|
6
|
+
import { canonicalJson } from "./portable-values.mjs";
|
|
7
|
+
import { objectAt, stringAt } from "./portable-shape.mjs";
|
|
8
|
+
import { portableStateDirectory } from "./portable-state.mjs";
|
|
9
|
+
import { validateProviderBinding } from "./resolution-shape.mjs";
|
|
10
|
+
import { validateExecutionBinding } from "./schedule-capsule.mjs";
|
|
11
|
+
import { oatsError } from "./errors.mjs";
|
|
12
|
+
|
|
13
|
+
export const CAPTURED_SOURCE_RECEIPT_LIMITS = Object.freeze({ maxBytes: 256 * 1024, maxDepth: 32, maxEntries: 16_384 });
|
|
14
|
+
const RECEIPT_FIELDS = ["schemaVersion", "kind", "home", "work", "context", "agent", "instance", "sourceIdentity", "role", "executionBinding", "responsibleHuman", "binding"];
|
|
15
|
+
|
|
16
|
+
function absolutePath(value, pointer) {
|
|
17
|
+
stringAt(value, pointer);
|
|
18
|
+
if (!isAbsolute(value) || resolve(value) !== value || value.includes("\0")) throw oatsError("invalid-declaration", `${pointer} must be a normalized absolute path`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function human(value) {
|
|
22
|
+
if (value === null) return;
|
|
23
|
+
objectAt(value, ["provider", "id"], ["provider", "id"], "/responsibleHuman");
|
|
24
|
+
stringAt(value.provider, "/responsibleHuman/provider");
|
|
25
|
+
stringAt(value.id, "/responsibleHuman/id");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateCapturedSourceReceipt(value) {
|
|
29
|
+
canonicalJson(value, CAPTURED_SOURCE_RECEIPT_LIMITS);
|
|
30
|
+
objectAt(value, RECEIPT_FIELDS, RECEIPT_FIELDS);
|
|
31
|
+
if (value.schemaVersion !== 1) throw oatsError("unsupported-wire-version", "captured source receipt schemaVersion must be 1");
|
|
32
|
+
if (!["persistent", "helper"].includes(value.kind)) throw oatsError("invalid-declaration", "captured source receipt kind must be persistent or helper");
|
|
33
|
+
for (const [key, field] of [["home", value.home], ["work", value.work], ["context", value.context]]) absolutePath(field, `/${key}`);
|
|
34
|
+
if (value.context !== value.executionBinding?.deployment) throw oatsError("invalid-declaration", "captured source receipt context must equal its execution deployment");
|
|
35
|
+
stringAt(value.agent, "/agent"); stringAt(value.instance, "/instance"); stringAt(value.role, "/role");
|
|
36
|
+
for (const [pointer, text] of [["/agent", value.agent], ["/instance", value.instance], ["/role", value.role]]) if (text.includes("\0")) throw oatsError("invalid-declaration", `${pointer} contains NUL`);
|
|
37
|
+
if (!value.agent || !value.instance || Buffer.byteLength(value.role, "utf8") > 128 * 1024) throw oatsError("invalid-declaration", "captured source receipt identity/role is empty or exceeds its bound");
|
|
38
|
+
if (value.kind === "persistent") {
|
|
39
|
+
if (value.sourceIdentity === null) throw oatsError("invalid-declaration", "persistent captured source receipt needs a qualified soul identity");
|
|
40
|
+
validateSoulIdentity(value.sourceIdentity);
|
|
41
|
+
} else if (value.sourceIdentity !== null) throw oatsError("invalid-declaration", "helper captured source receipt must not claim a persistent soul identity");
|
|
42
|
+
validateExecutionBinding(value.executionBinding); human(value.responsibleHuman); validateProviderBinding(value.binding);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function withCapturedSourceReceiptFile(home, receipt, run) {
|
|
47
|
+
validateCapturedSourceReceipt(receipt);
|
|
48
|
+
if (typeof run !== "function") throw oatsError("invalid-declaration", "captured source receipt consumer must be a function");
|
|
49
|
+
if (resolve(home) !== receipt.home) throw oatsError("invalid-declaration", "captured source receipt home differs from its invocation home");
|
|
50
|
+
const directory = mkdtempSync(join(portableStateDirectory(receipt.executionBinding.deployment, true), ".source-receipt-"));
|
|
51
|
+
const owned = lstatSync(directory), file = join(directory, "receipt.json"); let primary, result, completed = false;
|
|
52
|
+
try {
|
|
53
|
+
writeFileSync(file, canonicalJson(receipt, CAPTURED_SOURCE_RECEIPT_LIMITS), { flag: "wx", mode: 0o600 });
|
|
54
|
+
result = run({ OATS_SOURCE_RECEIPT_FILE: file }); completed = true; return result;
|
|
55
|
+
} catch (error) { primary = error; throw error; }
|
|
56
|
+
finally {
|
|
57
|
+
try {
|
|
58
|
+
const current = lstatSync(directory);
|
|
59
|
+
if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("integrity-drift", "source receipt invocation directory ownership changed");
|
|
60
|
+
rmSync(directory, { recursive: true });
|
|
61
|
+
} catch (cleanup) {
|
|
62
|
+
if (!primary) {
|
|
63
|
+
const error = new AggregateError([cleanup], "captured source receipt cleanup failed after invocation", { cause: cleanup });
|
|
64
|
+
error.code = cleanup.code; error.invocationCompleted = completed; error.invocationResult = result; throw error;
|
|
65
|
+
}
|
|
66
|
+
const error = new AggregateError([primary, cleanup], "captured source receipt invocation and cleanup failed", { cause: primary });
|
|
67
|
+
error.code = primary.code;
|
|
68
|
+
if (primary.invocationCompleted) { error.invocationCompleted = true; error.invocationResult = primary.invocationResult; }
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** One bounded decoder for new portable documents. YAML is syntax, not an
|
|
2
|
+
* object-construction language: no tags, anchors, aliases, merges or coercive keys.
|
|
3
|
+
* Legacy core readers are unchanged until the explicit consumer migration. */
|
|
4
|
+
import { Composer, CST, Lexer, Parser, isAlias, isMap, isScalar, isSeq } from "yaml";
|
|
5
|
+
import { bytesIntegrity } from "./portable-digest.mjs";
|
|
6
|
+
import { byteView, canonicalJson, dataLimits, decodeUtf8, parseStrictJson } from "./portable-values.mjs";
|
|
7
|
+
import { oatsError } from "./errors.mjs";
|
|
8
|
+
|
|
9
|
+
const pointerKey = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
10
|
+
const SYNTAX_NODES = new Set(["scalar", "single-quoted-scalar", "double-quoted-scalar", "map-value-ind", "seq-item-ind", "flow-map-start", "flow-seq-start", "doc-mode", "doc-start"]);
|
|
11
|
+
function boundedYaml(source, limits) {
|
|
12
|
+
const parser = new Parser();
|
|
13
|
+
// Duplicate decoded keys are checked in our linear own-key traversal below,
|
|
14
|
+
// not the composer's quadratic scan of every preceding mapping pair.
|
|
15
|
+
const composer = new Composer({ version: "1.2", schema: "core", uniqueKeys: false, strict: true, merge: false });
|
|
16
|
+
function* tokens() {
|
|
17
|
+
let nodes = 0;
|
|
18
|
+
for (const lexeme of new Lexer().lex(source)) {
|
|
19
|
+
const type = CST.tokenType(lexeme);
|
|
20
|
+
if (SYNTAX_NODES.has(type) && ++nodes > limits.maxEntries * 4 + 8) {
|
|
21
|
+
throw oatsError("resource-limit", "portable YAML syntax entry limit exceeded");
|
|
22
|
+
}
|
|
23
|
+
yield* parser.next(lexeme);
|
|
24
|
+
if (parser.stack.length > limits.maxDepth + 3) {
|
|
25
|
+
throw oatsError("resource-limit", "portable YAML syntax depth limit exceeded");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
yield* parser.end();
|
|
29
|
+
}
|
|
30
|
+
let result;
|
|
31
|
+
for (const document of composer.compose(tokens(), true, source.length)) {
|
|
32
|
+
if (result) throw oatsError("invalid-declaration", "multiple portable YAML documents are not supported");
|
|
33
|
+
result = document;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export function parseConfigData(input, { format = "auto", origin = null, limits: requested = {} } = {}) {
|
|
38
|
+
const limits = dataLimits({ maxBytes: 1024 * 1024, ...requested });
|
|
39
|
+
const data = typeof input === "string" ? input : byteView(input);
|
|
40
|
+
const size = typeof data === "string" ? Buffer.byteLength(data, "utf8") : data.byteLength;
|
|
41
|
+
if (size > limits.maxBytes) throw oatsError("resource-limit", "portable document byte limit exceeded");
|
|
42
|
+
const source = decodeUtf8(input);
|
|
43
|
+
canonicalJson(origin, { maxBytes: 64 * 1024, maxDepth: 16, maxEntries: 1024 });
|
|
44
|
+
if (!["auto", "json", "yaml"].includes(format)) throw oatsError("invalid-declaration", "unsupported configuration syntax");
|
|
45
|
+
const json = format === "json" || (format === "auto" && /^[\s]*[\[{]/.test(source));
|
|
46
|
+
const origins = Object.create(null);
|
|
47
|
+
let entries = 0;
|
|
48
|
+
const mark = (pointer, depth, node) => {
|
|
49
|
+
if (depth > limits.maxDepth || ++entries > limits.maxEntries) {
|
|
50
|
+
throw oatsError("resource-limit", "portable document depth/entry limit exceeded");
|
|
51
|
+
}
|
|
52
|
+
origins[pointer] = { document: origin, pointer };
|
|
53
|
+
if (node?.range) origins[pointer].span = { start: node.range[0], end: node.range[1] };
|
|
54
|
+
};
|
|
55
|
+
let value;
|
|
56
|
+
if (json) {
|
|
57
|
+
value = parseStrictJson(source, limits);
|
|
58
|
+
const walk = (item, pointer, depth) => {
|
|
59
|
+
mark(pointer, depth);
|
|
60
|
+
if (item && typeof item === "object") {
|
|
61
|
+
for (const [key, child] of Object.entries(item)) walk(child, `${pointer}/${pointerKey(key)}`, depth + 1);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
walk(value, "", 0);
|
|
65
|
+
} else {
|
|
66
|
+
let document;
|
|
67
|
+
try { document = boundedYaml(source, limits); }
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === "resource-limit" || error.code === "invalid-declaration") throw error;
|
|
70
|
+
throw oatsError(error instanceof RangeError ? "resource-limit" : "invalid-declaration", "portable YAML could not be decoded");
|
|
71
|
+
}
|
|
72
|
+
if (document.errors.length || document.warnings.length || document.directives.yaml.version !== "1.2") {
|
|
73
|
+
const issue = document.errors[0] ?? document.warnings[0];
|
|
74
|
+
throw oatsError("invalid-declaration", `invalid portable YAML${issue ? ` (${issue.code})` : " version"}`);
|
|
75
|
+
}
|
|
76
|
+
const decode = (node, pointer, depth) => {
|
|
77
|
+
mark(pointer, depth, node);
|
|
78
|
+
if (node === null) return null;
|
|
79
|
+
if (isAlias(node) || node.anchor || node.tag) {
|
|
80
|
+
throw oatsError("invalid-declaration", "YAML anchors, aliases and explicit tags are not portable data");
|
|
81
|
+
}
|
|
82
|
+
if (isScalar(node)) return node.value;
|
|
83
|
+
if (isSeq(node)) return node.items.map((child, i) => decode(child, `${pointer}/${i}`, depth + 1));
|
|
84
|
+
if (isMap(node)) {
|
|
85
|
+
const result = Object.create(null);
|
|
86
|
+
for (const pair of node.items) {
|
|
87
|
+
const key = pair.key;
|
|
88
|
+
if (!isScalar(key) || typeof key.value !== "string" || key.anchor || key.tag
|
|
89
|
+
|| (key.value === "<<" && key.type !== "QUOTE_DOUBLE" && key.type !== "QUOTE_SINGLE")) {
|
|
90
|
+
throw oatsError("invalid-declaration", "portable YAML mapping keys must be literal strings without merge syntax");
|
|
91
|
+
}
|
|
92
|
+
if (Object.hasOwn(result, key.value)) throw oatsError("invalid-declaration", "duplicate portable YAML key");
|
|
93
|
+
result[key.value] = decode(pair.value, `${pointer}/${pointerKey(key.value)}`, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
throw oatsError("invalid-declaration", "unsupported portable YAML node");
|
|
98
|
+
};
|
|
99
|
+
value = decode(document.contents, "", 0);
|
|
100
|
+
// Enforce Unicode-scalar/finite JSON semantics even for YAML core scalars.
|
|
101
|
+
canonicalJson(value, limits);
|
|
102
|
+
}
|
|
103
|
+
return { value, origins, integrity: bytesIntegrity(Buffer.from(source, "utf8")) };
|
|
104
|
+
}
|