@awebai/oats 0.25.9 → 0.26.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 +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +8 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
- package/docs/design/2026-09-24-phase-d-plan.md +57 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +537 -261
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +64 -33
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +6 -6
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/captured-pi-host.mjs
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/** Execution-side authority for the explicitly selected native SDK host.
|
|
2
|
-
* Reuses the kernel's actual retained loader and original indexed admission;
|
|
3
|
-
* it never selects a current config, creates an incarnation or admits an action. */
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { lstatSync, readlinkSync, readdirSync, realpathSync } from "node:fs";
|
|
6
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
-
import { nativeHistoryPath } from "../packages/record/lib/native-history.mjs";
|
|
8
|
-
import { validateCapturedSessionTarget } from "./captured-session-backend.mjs";
|
|
9
|
-
import { loadCapturedDispatch, applicableRequirements } from "./core.mjs";
|
|
10
|
-
import { readCapturedInstanceAuthority, assertCapturedInstanceCustody, capturedDirectoryIdentity } from "./captured-instance-index.mjs";
|
|
11
|
-
import { validateIntentRef } from "./captured-admission-shape.mjs";
|
|
12
|
-
import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
|
|
13
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
14
|
-
import { jsonIntegrity, treeIntegrity } from "./portable-digest.mjs";
|
|
15
|
-
import { assertCapturedPiStart, capturedPiSessionDirectory } from "./captured-pi-custody.mjs";
|
|
16
|
-
import { PI_SDK_HOST, parsePiHostRecipeArgs, parsePiHostArgv, piHostArgv, readPiHostTask, runPiSdkHost, validatePiHostRecipe } from "./pi-sdk-host.mjs";
|
|
17
|
-
import { readPiOutcome, summarizePiOutcome, writePiExitMarker, writePiOutcome } from "./captured-pi-outcome.mjs";
|
|
18
|
-
import { oatsError } from "./errors.mjs";
|
|
19
|
-
|
|
20
|
-
const same = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
21
|
-
const refuse = message => { throw oatsError("E_PI_HOST_CUSTODY", message); };
|
|
22
|
-
|
|
23
|
-
/** Relate the ORIGINAL process ref to the same logical admission. A retry may
|
|
24
|
-
* advance the index counter without redispatching that process. The exact
|
|
25
|
-
* pending ref remains the anchor; the caller MUST also validate its started-v2
|
|
26
|
-
* record/root, input, target, launch and curriculum before any SDK effect. */
|
|
27
|
-
export function verifyPiDispatchRef(row, intent, pending, model) {
|
|
28
|
-
validateIntentRef(intent);
|
|
29
|
-
const admitted = row.intents.find(value => value.executionId === intent.executionId);
|
|
30
|
-
const priorLiveAttempt = admitted && admitted.attempt > intent.attempt;
|
|
31
|
-
if (row.incarnationId !== intent.incarnationId || !admitted || !Number.isSafeInteger(admitted.attempt)
|
|
32
|
-
|| admitted.attempt < intent.attempt || admitted.capability !== null || admitted.action.kind !== "session"
|
|
33
|
-
|| !["start", "restart"].includes(admitted.action.name)
|
|
34
|
-
|| !(["running", "completed", "unconfirmed"].includes(admitted.state) || (priorLiveAttempt && admitted.state === "admitted"))) {
|
|
35
|
-
refuse("Pi host execution is not an admitted native action");
|
|
36
|
-
}
|
|
37
|
-
if (!pending || pending.id !== intent.executionId || !pending.capturedIntent || !same(pending.capturedIntent, intent)
|
|
38
|
-
|| typeof pending.nativeRecordId !== "string" || !/^[a-f0-9-]{36}$/.test(pending.nativeRecordId)
|
|
39
|
-
|| pending.runtime !== "pi" || pending.model !== model) refuse("Pi host lacks the original native dispatch receipt");
|
|
40
|
-
// Reconciliation may retain a partial observation or the completed adoption
|
|
41
|
-
// result. Neither may contradict the original pending dispatch. Absence is
|
|
42
|
-
// not root authority: the mandatory started-v2 check still supplies that.
|
|
43
|
-
if (priorLiveAttempt) {
|
|
44
|
-
const observed = admitted.receipt;
|
|
45
|
-
if ((observed?.id !== undefined && observed.id !== pending.id)
|
|
46
|
-
|| (observed?.nativeRecordId !== undefined && observed.nativeRecordId !== pending.nativeRecordId)
|
|
47
|
-
|| (observed?.target !== undefined && !same(observed.target, pending.target))) {
|
|
48
|
-
refuse("Pi reconciliation observation differs from the original dispatch");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return admitted;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function verifyCapturedPiHost(options, env = process.env) {
|
|
55
|
-
if (!env.OATS_DEPLOYMENT || !env.OATS_INCARNATION_ID || !env.OATS_EXECUTION_ID || !/^[1-9][0-9]*$/.test(env.OATS_EXECUTION_ATTEMPT ?? "")
|
|
56
|
-
|| env.OATS_INSTANCE_HOME !== options.home) refuse("Pi host has no original admitted execution context");
|
|
57
|
-
const intent = { schemaVersion: 1, incarnationId: env.OATS_INCARNATION_ID, executionId: env.OATS_EXECUTION_ID, attempt: Number(env.OATS_EXECUTION_ATTEMPT) };
|
|
58
|
-
return verifyCapturedPiContext(options, intent, { deployment: env.OATS_DEPLOYMENT, resolutionId: env.OATS_RESOLUTION, instance: env.OATS_INSTANCE });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Shared read-only verification, NOT an invocation/env constructor. Inspecting
|
|
62
|
-
// observed facts does not impersonate a process or admit another action.
|
|
63
|
-
function verifyCapturedPiContext(options, intent, selector) {
|
|
64
|
-
const { home, sessionDir } = options;
|
|
65
|
-
validateIntentRef(intent);
|
|
66
|
-
const { row, metadata } = readCapturedInstanceAuthority(selector.deployment, home);
|
|
67
|
-
if (row.incarnationId !== intent.incarnationId || row.executionBinding.resolution.id !== selector.resolutionId
|
|
68
|
-
|| selector.instance !== row.instance) refuse("Pi host invocation differs from original captured custody");
|
|
69
|
-
const pending = parseStrictJson(readPortableBytes(join(home, ".oats-start-pending.json")));
|
|
70
|
-
const admitted = verifyPiDispatchRef(row, intent, pending, options.model);
|
|
71
|
-
validateCapturedSessionTarget(pending.target, metadata.captured.nativeBackend, row.instance);
|
|
72
|
-
if (pending.target.backend === "herdr" && !same(admitted.receipt?.target, pending.target)) refuse("Pi Herdr target lacks indexed observation");
|
|
73
|
-
if (!row.nativeScaffold || !same(row.nativeScaffold, metadata.captured.nativeScaffold)) refuse("Pi native scaffold authority changed");
|
|
74
|
-
const history = nativeHistoryPath(home);
|
|
75
|
-
for (const [key, path] of Object.entries({ historyRoot: dirname(history), history })) {
|
|
76
|
-
if (!same(capturedDirectoryIdentity(path), row.nativeScaffold.directories[key])) refuse("Pi native record custody directory changed");
|
|
77
|
-
}
|
|
78
|
-
if (sessionDir !== capturedPiSessionDirectory(home)) refuse("Pi session directory differs from the kernel-selected root");
|
|
79
|
-
assertCapturedPiStart(home, pending.nativeRecordId, { incarnationId: row.incarnationId, intent, sessionDir });
|
|
80
|
-
const loaded = loadCapturedDispatch({ deployment: row.executionBinding.deployment, resolution: row.executionBinding.resolution, action: { kind: "compose" } });
|
|
81
|
-
const launch = loaded.record.dispatch.launch;
|
|
82
|
-
const profile = validatePiHostRecipe(launch);
|
|
83
|
-
if (launch.executable !== PI_SDK_HOST || !same({ ...launch, hooks: { launch: {}, env: {}, contributions: [] } }, pending.launch) || launch.model !== options.model
|
|
84
|
-
|| !same(profile, parsePiHostRecipeArgs(["--oats-pi-host", options.hostVersion, "--mode", options.mode, "--thinking", options.thinkingLevel, "--sdk-root", options.sdkRoot, "--sdk-version", options.sdkVersion]))) refuse("Pi host differs from the exact retained launch selection");
|
|
85
|
-
if (loaded.record.dispatch.composition?.mode !== "directory" || loaded.record.dispatch.runtimePackages.length
|
|
86
|
-
|| applicableRequirements("pi", [...loaded.capabilities.values()]).length
|
|
87
|
-
|| [...loaded.capabilities.values()].some(cap => cap.manifest.hooks?.launch)
|
|
88
|
-
|| row.intents.some(value => value.action.kind === "hook" && value.action.name === "spawn" && !value.replayable)
|
|
89
|
-
|| loaded.approvals.some(value => !["approved", "not-required"].includes(value.status))) refuse("retained source is not eligible for the zero-plugin host");
|
|
90
|
-
const { bytes } = readPiHostTask(options.taskFile);
|
|
91
|
-
const input = { backend: metadata.captured.nativeBackend, taskIntegrity: { format: "oats.bytes.v1", value: `sha256-${createHash("sha256").update(bytes).digest("hex")}` } };
|
|
92
|
-
if (!same(jsonIntegrity(input), admitted.inputIntegrity)) refuse("Pi task/backend differs from the admitted input");
|
|
93
|
-
const agentsPath = join(home, "AGENTS.md");
|
|
94
|
-
if (readPortableBytes(agentsPath).toString("utf8") !== loaded.composition.text
|
|
95
|
-
|| !lstatSync(join(home, "CLAUDE.md")).isSymbolicLink() || readlinkSync(join(home, "CLAUDE.md")) !== "AGENTS.md"
|
|
96
|
-
|| !lstatSync(join(home, "soul")).isSymbolicLink() || realpathSync(join(home, "soul")) !== dirname(loaded.resources.get(loaded.record.dispatch.composition.body))) refuse("Pi home curriculum differs from retained composition");
|
|
97
|
-
if (!same(readdirSync(join(home, ".agents/skills")).sort(), loaded.composition.skills.map(skill => skill.name).sort())) refuse("Pi projected skill inventory changed");
|
|
98
|
-
for (const skill of loaded.composition.skills) {
|
|
99
|
-
if (!same(treeIntegrity(join(home, ".agents/skills", skill.name)), treeIntegrity(skill.path))) refuse("Pi projected skill bytes changed");
|
|
100
|
-
}
|
|
101
|
-
const current = assertCapturedInstanceCustody(row);
|
|
102
|
-
const currentPending = parseStrictJson(readPortableBytes(join(home, ".oats-start-pending.json")));
|
|
103
|
-
const currentIntent = verifyPiDispatchRef(current, intent, currentPending, options.model);
|
|
104
|
-
if (!same(currentPending, pending) || !same(currentIntent.inputIntegrity, admitted.inputIntegrity)) refuse("Pi dispatch evidence changed during verification");
|
|
105
|
-
// Keep the process's ORIGINAL ref, never the advanced reconciliation counter.
|
|
106
|
-
assertCapturedPiStart(home, pending.nativeRecordId, { incarnationId: row.incarnationId, intent, sessionDir });
|
|
107
|
-
return { cwd: home, agentsPath, text: loaded.composition.text,
|
|
108
|
-
skills: loaded.composition.skills.map(skill => ({ name: skill.name, path: join(skill.path, "SKILL.md") })),
|
|
109
|
-
outcomeAuthority: { home, sessionDir, incarnationId: row.incarnationId, intent,
|
|
110
|
-
nativeRecordId: pending.nativeRecordId, executionBinding: row.executionBinding,
|
|
111
|
-
target: pending.target, inputIntegrity: admitted.inputIntegrity,
|
|
112
|
-
selected: { runtime: "pi", ...options.modelSelection, sdkVersion: options.sdkVersion } } };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function runCapturedPiSdkHost(argv) {
|
|
116
|
-
return runPiSdkHost(argv, { verifyContext: options => verifyCapturedPiHost(options),
|
|
117
|
-
onOutcome: (options, observed, verified) => writePiOutcome("sdk", verified.outcomeAuthority, observed,
|
|
118
|
-
() => { if (!same(verifyCapturedPiHost(options).outcomeAuthority, verified.outcomeAuthority)) refuse("Pi outcome custody changed"); }) });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function outcomeOptions(home, nativeRecordId) {
|
|
122
|
-
const pending = parseStrictJson(readPortableBytes(join(home, ".oats-start-pending.json")));
|
|
123
|
-
if (pending.nativeRecordId !== nativeRecordId) refuse("Pi outcome is not the original current dispatch; historical re-selection is unsupported");
|
|
124
|
-
return { pending, options: parsePiHostArgv(piHostArgv(pending.launch, { home, sessionDir: capturedPiSessionDirectory(home) })) };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** PRIVATE launcher entry, not part of the accepted launch recipe grammar.
|
|
128
|
-
* exitCode is the enclosing launcher's saved `$?`, not runPrintMode's return. */
|
|
129
|
-
export function parsePiExitArgv(argv) {
|
|
130
|
-
const keys = ["--oats-pi-record-exit", "--home", "--native-record", "--exit-status"];
|
|
131
|
-
if (!Array.isArray(argv) || argv.length !== 8 || keys.some((key, i) => argv[i * 2] !== key)
|
|
132
|
-
|| argv[1] !== "1" || typeof argv[3] !== "string" || !isAbsolute(argv[3]) || resolve(argv[3]) !== argv[3] || argv[3].includes("\0")
|
|
133
|
-
|| typeof argv[5] !== "string" || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(argv[5])
|
|
134
|
-
|| typeof argv[7] !== "string" || !/^(0|[1-9][0-9]{0,2})$/.test(argv[7]) || Number(argv[7]) > 255) {
|
|
135
|
-
throw oatsError("E_PI_HOST_ARGS", "invalid private Pi process observation arguments");
|
|
136
|
-
}
|
|
137
|
-
return { home: argv[3], nativeRecordId: argv[5], exitCode: Number(argv[7]) };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function recordCapturedPiExit(argv, env = process.env) {
|
|
141
|
-
const { home, nativeRecordId, exitCode } = parsePiExitArgv(argv);
|
|
142
|
-
if (env.OATS_INSTANCE_HOME !== home || !env.OATS_DEPLOYMENT) refuse("Pi exit observer has no original home context");
|
|
143
|
-
readCapturedInstanceAuthority(env.OATS_DEPLOYMENT, home); // before reading pending paths
|
|
144
|
-
const { options } = outcomeOptions(home, nativeRecordId);
|
|
145
|
-
const authority = verifyCapturedPiHost(options, env).outcomeAuthority;
|
|
146
|
-
const check = () => { if (!same(verifyCapturedPiHost(options, env).outcomeAuthority, authority)) refuse("Pi process observation custody changed"); };
|
|
147
|
-
writePiOutcome("process", authority, { exitCode, source: "launcher-wait-status" }, check);
|
|
148
|
-
writePiExitMarker(authority, check);
|
|
149
|
-
return 0; // observer status ONLY; enclosing shell retains the native status
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Explicit public read-only query for the current ORIGINAL pending dispatch.
|
|
153
|
-
* No backend/model call, process-env impersonation, admission or provisioning. */
|
|
154
|
-
export function inspectCapturedPiOutcome(home, { deployment, resolution, nativeRecordId }) {
|
|
155
|
-
const { row } = readCapturedInstanceAuthority(deployment, home);
|
|
156
|
-
if (!same(row.executionBinding.resolution, resolution)) refuse("Pi outcome selector differs from original captured home");
|
|
157
|
-
const { options, pending } = outcomeOptions(home, nativeRecordId);
|
|
158
|
-
const selector = { deployment, resolutionId: resolution.id, instance: row.instance };
|
|
159
|
-
// Strict JSON decoding uses null-prototype maps. Reify the SAME validated wire
|
|
160
|
-
// ref as ordinary data for the record API; never advance its attempt or mint it.
|
|
161
|
-
validateIntentRef(pending.capturedIntent);
|
|
162
|
-
const intent = { ...pending.capturedIntent };
|
|
163
|
-
const authority = verifyCapturedPiContext(options, intent, selector).outcomeAuthority;
|
|
164
|
-
const check = () => { if (!same(verifyCapturedPiContext(options, intent, selector).outcomeAuthority, authority)) refuse("Pi outcome inspection custody changed"); };
|
|
165
|
-
const sdk = readPiOutcome("sdk", authority, check), process = readPiOutcome("process", authority, check);
|
|
166
|
-
return summarizePiOutcome(authority, sdk, process, check);
|
|
167
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/** Bounded, non-authorizing observations in the EXISTING witnessed Pi history
|
|
2
|
-
* directory. These JSON files are NOT native JSONL transcripts or root proofs. */
|
|
3
|
-
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readSync, realpathSync, renameSync, writeSync } from "node:fs";
|
|
4
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
-
import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
|
|
6
|
-
import { objectAt } from "./portable-shape.mjs";
|
|
7
|
-
import { oatsError } from "./errors.mjs";
|
|
8
|
-
|
|
9
|
-
const bad = message => { throw oatsError("E_PI_HOST_OUTCOME", message); };
|
|
10
|
-
const same = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
11
|
-
const limits = { maxBytes: 32768, maxDepth: 16, maxEntries: 512 };
|
|
12
|
-
const code = value => Number.isInteger(value) && value >= 0 && value <= 255;
|
|
13
|
-
const text = value => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
|
|
14
|
-
|
|
15
|
-
export function piOutcomePath(sessionDir, nativeRecordId, kind) {
|
|
16
|
-
if (!isAbsolute(sessionDir) || resolve(sessionDir) !== sessionDir || sessionDir.includes("\0")
|
|
17
|
-
|| typeof nativeRecordId !== "string" || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(nativeRecordId) || !["sdk", "process"].includes(kind)) bad("invalid Pi outcome locator");
|
|
18
|
-
return join(sessionDir, `.oats-pi-${kind}-${nativeRecordId}.json`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function syncDirectory(path, check) {
|
|
22
|
-
check();
|
|
23
|
-
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
24
|
-
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
25
|
-
check();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Exclusive publication; uncertain/partial/existing evidence is retained, not
|
|
29
|
-
* overwritten, adopted or cleaned to manufacture a successful observation. */
|
|
30
|
-
function writeOnce(path, bytes, check) {
|
|
31
|
-
check();
|
|
32
|
-
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
33
|
-
const identity = fstatSync(fd);
|
|
34
|
-
const checkFile = () => {
|
|
35
|
-
check();
|
|
36
|
-
const current = lstatSync(path);
|
|
37
|
-
if (!current.isFile() || current.dev !== identity.dev || current.ino !== identity.ino) bad("Pi outcome file was replaced during publication");
|
|
38
|
-
};
|
|
39
|
-
try {
|
|
40
|
-
checkFile();
|
|
41
|
-
let offset = 0;
|
|
42
|
-
while (offset < bytes.length) {
|
|
43
|
-
const count = writeSync(fd, bytes, offset, bytes.length - offset);
|
|
44
|
-
if (count <= 0) bad("Pi outcome write did not complete");
|
|
45
|
-
offset += count;
|
|
46
|
-
}
|
|
47
|
-
fsyncSync(fd); checkFile();
|
|
48
|
-
} finally { closeSync(fd); }
|
|
49
|
-
syncDirectory(dirname(path), check);
|
|
50
|
-
checkFile();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function validateObservation(observed) {
|
|
54
|
-
objectAt(observed, ["exitCode", "observation"], ["exitCode", "observation"]);
|
|
55
|
-
if (!code(observed.exitCode)) bad("SDK outcome lacks an observed numeric return status");
|
|
56
|
-
const o = observed.observation;
|
|
57
|
-
if (o === null) return;
|
|
58
|
-
objectAt(o, ["sdkVersion", "header", "sessionId", "sessionFile", "model", "finalAssistant"], ["sdkVersion", "header", "sessionId", "sessionFile", "model", "finalAssistant"]);
|
|
59
|
-
for (const field of ["sdkVersion", "sessionId", "sessionFile"]) if (o[field] !== null && !text(o[field])) bad("invalid SDK metadata");
|
|
60
|
-
if (o.header !== null) {
|
|
61
|
-
objectAt(o.header, ["type", "version", "id", "cwd", "timestamp"], ["type", "version", "id", "cwd", "timestamp"]);
|
|
62
|
-
if (o.header.type !== "session" || !Number.isSafeInteger(o.header.version) || !["id", "cwd", "timestamp"].every(k => text(o.header[k]))) bad("invalid observed SDK header");
|
|
63
|
-
}
|
|
64
|
-
if (o.model !== null) {
|
|
65
|
-
objectAt(o.model, ["provider", "id"], ["provider", "id"]);
|
|
66
|
-
if (!text(o.model.provider) || !text(o.model.id)) bad("invalid observed SDK model");
|
|
67
|
-
}
|
|
68
|
-
if (o.finalAssistant !== null) {
|
|
69
|
-
const a = o.finalAssistant;
|
|
70
|
-
objectAt(a, ["entryId", "provider", "model", "responseModel", "stopReason", "timestamp"], ["entryId", "provider", "model", "responseModel", "stopReason", "timestamp"]);
|
|
71
|
-
if (!["entryId", "provider", "model"].every(k => text(a[k])) || (a.responseModel !== null && !text(a.responseModel))
|
|
72
|
-
|| !["stop", "length", "toolUse", "error", "aborted"].includes(a.stopReason) || !Number.isFinite(a.timestamp) || a.timestamp < 0) bad("invalid final assistant metadata");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function validateReceipt(value, kind, authority) {
|
|
77
|
-
canonicalJson(value, limits);
|
|
78
|
-
objectAt(value, ["schemaVersion", "kind", "authority", "observed"], ["schemaVersion", "kind", "authority", "observed"]);
|
|
79
|
-
if (value.schemaVersion !== 1 || value.kind !== `oats.pi-${kind}-outcome` || !same(value.authority, authority)) bad("Pi outcome differs from original dispatch authority");
|
|
80
|
-
if (kind === "sdk") validateObservation(value.observed);
|
|
81
|
-
else {
|
|
82
|
-
objectAt(value.observed, ["exitCode", "source"], ["exitCode", "source"]);
|
|
83
|
-
if (!code(value.observed.exitCode) || value.observed.source !== "launcher-wait-status") bad("process outcome lacks actual launcher status");
|
|
84
|
-
}
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function writePiOutcome(kind, authority, observed, check) {
|
|
89
|
-
const receipt = { schemaVersion: 1, kind: `oats.pi-${kind}-outcome`, authority, observed };
|
|
90
|
-
validateReceipt(receipt, kind, authority);
|
|
91
|
-
writeOnce(piOutcomePath(authority.sessionDir, authority.nativeRecordId, kind), Buffer.from(canonicalJson(receipt, limits)), check);
|
|
92
|
-
return receipt;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Preserve the legacy ID-only marker. Called AFTER process outcome publication,
|
|
96
|
-
* under the same original home/root guards, never by an unguarded shell write. */
|
|
97
|
-
export function writePiExitMarker(authority, check) {
|
|
98
|
-
const temporary = join(authority.home, `.oats-start-exited.${authority.nativeRecordId}.tmp`);
|
|
99
|
-
writeOnce(temporary, Buffer.from(authority.intent.executionId + "\n"), check);
|
|
100
|
-
check();
|
|
101
|
-
renameSync(temporary, join(authority.home, ".oats-start-exited"));
|
|
102
|
-
syncDirectory(authority.home, check);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function readPiOutcome(kind, authority, check) {
|
|
106
|
-
const path = piOutcomePath(authority.sessionDir, authority.nativeRecordId, kind);
|
|
107
|
-
check();
|
|
108
|
-
let before;
|
|
109
|
-
try { before = lstatSync(path); }
|
|
110
|
-
catch (error) { if (error.code !== "ENOENT") throw error; check(); return null; }
|
|
111
|
-
if (!before.isFile() || before.size > limits.maxBytes) bad("Pi outcome is not a bounded regular file");
|
|
112
|
-
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
113
|
-
try {
|
|
114
|
-
const assertNamedDescriptor = () => {
|
|
115
|
-
// A pre-open stat can ALSO belong to a redirected ancestor. Bind the FD
|
|
116
|
-
// to the CURRENT physical name under original-root proof before reading,
|
|
117
|
-
// not merely to that earlier stat or a root that was restored after open.
|
|
118
|
-
check();
|
|
119
|
-
const named = lstatSync(path), opened = fstatSync(fd);
|
|
120
|
-
if (realpathSync(path) !== path) bad("Pi outcome path is not physical");
|
|
121
|
-
for (const stat of [named, opened]) {
|
|
122
|
-
if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino
|
|
123
|
-
|| stat.size !== before.size || stat.mtimeMs !== before.mtimeMs || stat.ctimeMs !== before.ctimeMs) bad("Pi outcome descriptor differs from its witnessed name");
|
|
124
|
-
}
|
|
125
|
-
check();
|
|
126
|
-
};
|
|
127
|
-
assertNamedDescriptor();
|
|
128
|
-
const bytes = Buffer.alloc(before.size + 1);
|
|
129
|
-
let size = 0;
|
|
130
|
-
while (size < bytes.length) {
|
|
131
|
-
assertNamedDescriptor();
|
|
132
|
-
const n = readSync(fd, bytes, size, bytes.length - size, null);
|
|
133
|
-
if (n === 0) break;
|
|
134
|
-
size += n;
|
|
135
|
-
}
|
|
136
|
-
assertNamedDescriptor();
|
|
137
|
-
if (size !== before.size) bad("Pi outcome changed while reading");
|
|
138
|
-
return validateReceipt(parseStrictJson(bytes.subarray(0, size), limits), kind, authority);
|
|
139
|
-
} finally { closeSync(fd); }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Qualifies ONLY native-print completion, never learning/privacy/readiness or
|
|
143
|
-
* another action. Actual root checks surround receipt/session-path reads. */
|
|
144
|
-
export function summarizePiOutcome(authority, sdk, process, check) {
|
|
145
|
-
if (sdk) validateReceipt(sdk, "sdk", authority);
|
|
146
|
-
if (process) validateReceipt(process, "process", authority);
|
|
147
|
-
const observed = sdk?.observed.observation, selected = authority.selected;
|
|
148
|
-
let sessionFileObserved = false;
|
|
149
|
-
if (observed?.sessionFile) {
|
|
150
|
-
const file = observed.sessionFile;
|
|
151
|
-
if (!isAbsolute(file) || resolve(file) !== file || dirname(file) !== authority.sessionDir || !file.endsWith(".jsonl")) bad("SDK outcome names an unowned session file");
|
|
152
|
-
check();
|
|
153
|
-
try { sessionFileObserved = lstatSync(file).isFile() && realpathSync(file) === file; }
|
|
154
|
-
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
155
|
-
check();
|
|
156
|
-
}
|
|
157
|
-
const final = observed?.finalAssistant;
|
|
158
|
-
const finalObserved = !!(sessionFileObserved && observed.sdkVersion === selected.sdkVersion
|
|
159
|
-
&& observed.header?.version === 3 && observed.header.cwd === authority.home
|
|
160
|
-
&& observed.header.id === observed.sessionId && observed.model?.provider === selected.provider
|
|
161
|
-
&& observed.model.id === selected.id && final?.provider === selected.provider && final.model === selected.id);
|
|
162
|
-
const processExitCode = process?.observed.exitCode ?? null, sdkExitCode = sdk?.observed.exitCode ?? null;
|
|
163
|
-
const failed = (processExitCode !== null && processExitCode !== 0) || (sdkExitCode !== null && sdkExitCode !== 0)
|
|
164
|
-
|| (finalObserved && ["error", "aborted"].includes(final.stopReason));
|
|
165
|
-
const qualified = !failed && processExitCode === 0 && sdkExitCode === 0 && finalObserved && final.stopReason === "stop";
|
|
166
|
-
check();
|
|
167
|
-
return { schemaVersion: 1, contract: "oats.pi-print-completion", nonAuthorizing: true,
|
|
168
|
-
authority, status: qualified ? "succeeded" : failed ? "failed" : "incomplete", qualified,
|
|
169
|
-
processExitCode, sdkExitCode, finalObserved, sdk: observed ?? null,
|
|
170
|
-
// No default success when either observer, native header or final turn is absent.
|
|
171
|
-
evidence: { process: process !== null, sdk: sdk !== null, sessionFile: sessionFileObserved } };
|
|
172
|
-
}
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/** Immutable resolution records outside homes. Exact input verification is not
|
|
2
|
-
* approval, enrollment or host readiness; dispatch must check those separately. */
|
|
3
|
-
import { linkSync, lstatSync, mkdirSync, mkdtempSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { readPortableBytes } from "./portable-files.mjs";
|
|
5
|
-
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
6
|
-
import { ensureStoreIgnore } from "./capability-artifacts.mjs";
|
|
7
|
-
import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
|
|
8
|
-
import { bytesIntegrity, jsonIntegrity, PACKAGE_FORMAT, treeIntegrity } from "./portable-digest.mjs";
|
|
9
|
-
import { settingChoiceKey, soulConstraints } from "./soul-constraints.mjs";
|
|
10
|
-
import { verifyManifestSettings } from "./manifest-settings.mjs";
|
|
11
|
-
import { verifyHelperInjectionPolicies } from "./helper-injection-policy.mjs";
|
|
12
|
-
import { verifyPortableArtifact } from "./portable-artifacts.mjs";
|
|
13
|
-
import { validateResolutionRef, validateResolutionShape } from "./resolution-shape.mjs";
|
|
14
|
-
import { parsePortableSoul } from "./portable-soul.mjs";
|
|
15
|
-
import { parseConfigData } from "./config-data.mjs";
|
|
16
|
-
import { normalizePackagePath } from "./capability-provenance.mjs";
|
|
17
|
-
import { parseLockedSource3, parseRepositorySource } from "./source-spec.mjs";
|
|
18
|
-
import { oatsError } from "./errors.mjs";
|
|
19
|
-
|
|
20
|
-
const exists = (path) => { try { return lstatSync(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } };
|
|
21
|
-
const outside = (root, target) => { const part = relative(root, target); return part === ".." || part.startsWith("../") || part.startsWith("..\\") || isAbsolute(part); };
|
|
22
|
-
function store(scope, create) {
|
|
23
|
-
if (typeof scope !== "string" || !isAbsolute(scope)) throw oatsError("invalid-resolution", "resolution deployment must be explicit and absolute");
|
|
24
|
-
let root;
|
|
25
|
-
try { root = realpathSync(scope); }
|
|
26
|
-
catch (error) { if (error.code === "ENOENT") throw oatsError("resolution-not-found", "resolution deployment is absent"); throw error; }
|
|
27
|
-
if (!lstatSync(root).isDirectory()) throw oatsError("invalid-resolution", "resolution deployment is not a directory");
|
|
28
|
-
for (const name of [".agents", "resolutions"]) {
|
|
29
|
-
root = join(root, name);
|
|
30
|
-
if (create) { try { mkdirSync(root, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") throw error; } }
|
|
31
|
-
const stat = exists(root);
|
|
32
|
-
if (!stat) throw oatsError("resolution-not-found", "resolution store is absent");
|
|
33
|
-
if (!stat.isDirectory()) throw oatsError("invalid-resolution", "managed resolution store component is not a real directory");
|
|
34
|
-
}
|
|
35
|
-
if (create) ensureStoreIgnore(root);
|
|
36
|
-
return root;
|
|
37
|
-
}
|
|
38
|
-
const readBytes = (path, missingCode = "resolution-not-found") => readPortableBytes(path, { missingCode, invalidCode: "invalid-resolution" });
|
|
39
|
-
function decodeRecord(path, reference) {
|
|
40
|
-
const bytes = readBytes(path), value = parseStrictJson(bytes);
|
|
41
|
-
if (jsonIntegrity(value).value !== reference.id || !bytes.equals(Buffer.from(canonicalJson(value)))) {
|
|
42
|
-
throw oatsError("integrity-drift", "captured resolution bytes differ from their canonical address");
|
|
43
|
-
}
|
|
44
|
-
// Check the address before inspecting/following any references.
|
|
45
|
-
validateResolutionShape(value);
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
export function readCapturedResolution(scope, reference) {
|
|
49
|
-
validateResolutionRef(reference);
|
|
50
|
-
return decodeRecord(join(store(scope, false), `${reference.id}.json`), reference);
|
|
51
|
-
}
|
|
52
|
-
function resourcePath(root, reference) {
|
|
53
|
-
const path = join(root, reference.path);
|
|
54
|
-
let actual;
|
|
55
|
-
try { actual = realpathSync(path); }
|
|
56
|
-
catch (error) { if (error.code === "ENOENT") throw oatsError("resource-not-found", "captured resource is absent"); throw error; }
|
|
57
|
-
if (outside(root, actual)) throw oatsError("resource-not-contained", "captured resource escapes its artifact");
|
|
58
|
-
const stat = lstatSync(actual), directory = ["directory", "skill", "runtime-package"].includes(reference.kind);
|
|
59
|
-
if (directory ? !stat.isDirectory() : !stat.isFile()) throw oatsError("resource-not-found", "captured resource kind differs from its declaration");
|
|
60
|
-
return actual;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function canonicalSoulFiles(definition) {
|
|
64
|
-
const body = join(dirname(definition), "AGENTS.md"), alias = join(dirname(definition), "CLAUDE.md");
|
|
65
|
-
if (!exists(body)?.isFile() || !exists(alias)?.isSymbolicLink() || isAbsolute(readlinkSync(alias)) || realpathSync(alias) !== realpathSync(body)) {
|
|
66
|
-
throw oatsError("resolution-incomplete", "captured soul lacks its canonical AGENTS.md/CLAUDE.md alias");
|
|
67
|
-
}
|
|
68
|
-
if (exists(join(dirname(definition), "knowledge"))) throw oatsError("resolution-incomplete", "portable soul knowledge must be external");
|
|
69
|
-
}
|
|
70
|
-
function verifySoulConstraints(record, root, definition) {
|
|
71
|
-
const source = record.subject.soul;
|
|
72
|
-
const localBase = source.revision.kind === "local" ? parseLockedSource3(source.revision.source, ".").localPath
|
|
73
|
-
: record.choices["/bindings/source/localBase"]?.value;
|
|
74
|
-
// Local addresses are only compared to already captured provenance here;
|
|
75
|
-
// parsing never acquires or reads the original local directory.
|
|
76
|
-
const definitionBytes = readBytes(definition, "resource-not-found");
|
|
77
|
-
const sourceDocument = { kind: "source", source: source.revision.kind === "local" ? source.revision.source : source.revision.remote,
|
|
78
|
-
revision: source.revision.kind === "local" ? "local" : source.revision.commit,
|
|
79
|
-
path: source.definition, integrity: bytesIntegrity(definitionBytes) };
|
|
80
|
-
const parsed = parsePortableSoul(definitionBytes, { origin: sourceDocument, localBase, allowLocalPaths: true });
|
|
81
|
-
const originKey = ({ kind, document, pointer }) => canonicalJson({ kind, document, pointer });
|
|
82
|
-
for (const expected of soulConstraints(parsed)) {
|
|
83
|
-
const choice = record.choices[expected.key];
|
|
84
|
-
const retained = choice?.constraints.some((constraint) => constraint.kind === expected.kind
|
|
85
|
-
&& (expected.kind !== "equals" || canonicalJson(constraint.value) === canonicalJson(expected.value))
|
|
86
|
-
&& originKey(constraint.origin) === originKey(expected.origin));
|
|
87
|
-
if (!retained) throw oatsError("resolution-incomplete", "captured choice omits its source hard constraint or provenance");
|
|
88
|
-
}
|
|
89
|
-
const sourcePath = (path) => {
|
|
90
|
-
if (!source.projection.roots.some((entry) => entry === "." || path === entry || path.startsWith(`${entry}/`))) {
|
|
91
|
-
throw oatsError("resolution-incomplete", "declared source resource is not in the captured projection");
|
|
92
|
-
}
|
|
93
|
-
const file = join(root, path);
|
|
94
|
-
if (!exists(file)) throw oatsError("resource-not-found", "declared source resource is absent");
|
|
95
|
-
if (outside(root, realpathSync(file))) throw oatsError("resource-not-contained", "declared source resource escapes its artifact");
|
|
96
|
-
};
|
|
97
|
-
for (const path of parsed.declaration.resources ?? []) sourcePath(path);
|
|
98
|
-
const check = (id, selection, pointer) => {
|
|
99
|
-
const row = record.artifacts.capabilities[id];
|
|
100
|
-
if (!row) throw oatsError("resolution-incomplete", "source hard requirement is not captured");
|
|
101
|
-
const required = parsed.sources[`${pointer}/source`];
|
|
102
|
-
const pkg = row.origin.kind === "package" ? record.artifacts.packages[row.origin.package] : null;
|
|
103
|
-
const actual = pkg ? parseLockedSource3(pkg.source, pkg.path) : parseLockedSource3(row.origin.source, ".");
|
|
104
|
-
let matches;
|
|
105
|
-
if (required.kind === "repo") {
|
|
106
|
-
sourcePath(required.path);
|
|
107
|
-
if (!pkg || canonicalJson(treeIntegrity(join(root, required.path), { format: PACKAGE_FORMAT })) !== canonicalJson(pkg.integrity)) {
|
|
108
|
-
throw oatsError("resolution-incomplete", "repo package payload differs from its retained source snapshot");
|
|
109
|
-
}
|
|
110
|
-
matches = source.revision.kind === "local"
|
|
111
|
-
? actual.kind === "path" && actual.localPath === join(localBase, required.path)
|
|
112
|
-
: pkg && actual.kind === "git" && pkg.commit === source.revision.commit && pkg.path === required.path
|
|
113
|
-
&& actual.url === parseRepositorySource(source.revision.remote).url;
|
|
114
|
-
} else matches = required.kind === actual.kind && required.source === actual.source && required.path === actual.path;
|
|
115
|
-
if (!matches) throw oatsError("resolution-incomplete", "captured capability source violates its soul requirement");
|
|
116
|
-
for (const [key, expected] of Object.entries(selection.settings ?? {})) {
|
|
117
|
-
const choiceKey = record.dispatch.settingsChoices[id]?.[key];
|
|
118
|
-
if (choiceKey !== settingChoiceKey(id, key) || !Object.hasOwn(record.choices, choiceKey)
|
|
119
|
-
|| canonicalJson(record.choices[choiceKey].value) !== canonicalJson(expected)) {
|
|
120
|
-
throw oatsError("resolution-incomplete", "captured setting violates its soul requirement");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
const requirements = parsed.declaration.requires ?? {};
|
|
125
|
-
for (const [id, selection] of Object.entries(requirements.capabilities ?? {})) {
|
|
126
|
-
check(id, selection, `/requires/capabilities/${id.replace(/~/g, "~0").replace(/\//g, "~1")}`);
|
|
127
|
-
}
|
|
128
|
-
for (const slot of ["knowledge", "messaging", "tasks"]) {
|
|
129
|
-
if (!Object.hasOwn(requirements, slot)) continue;
|
|
130
|
-
const provider = record.bindings[slot];
|
|
131
|
-
if (!provider) throw oatsError("resolution-incomplete", "source requires an unbound fundamental provider");
|
|
132
|
-
const required = requirements[slot];
|
|
133
|
-
if (required !== "any") {
|
|
134
|
-
if (provider.capability !== required.capability) throw oatsError("resolution-incomplete", "captured provider violates its soul requirement");
|
|
135
|
-
check(required.capability, required, `/requires/${slot}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (parsed.declaration.knowledge) {
|
|
139
|
-
const binding = record.bindings.knowledge, declared = parsed.declaration.knowledge;
|
|
140
|
-
if (!binding || binding.payloadContract !== declared.contract || binding.payloadVersion !== declared.version) {
|
|
141
|
-
throw oatsError("resolution-incomplete", "captured knowledge binding does not support its source declaration");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Shared row/installation proof for exact record inputs and prospective
|
|
147
|
-
* artifact-set approval. The caller has already verified the artifact tree. */
|
|
148
|
-
export function verifyRetainedCapability(root, artifacts, id, manifest = parseStrictJson(readBytes(join(root, "oats.json"), "resource-not-found"))) {
|
|
149
|
-
const row = artifacts.capabilities[id];
|
|
150
|
-
if (!row || manifest.capability !== id || manifest.version !== row.version) throw oatsError("invalid-resolution", "retained manifest identity/version differs from its captured row");
|
|
151
|
-
if (row.origin.kind === "package") {
|
|
152
|
-
const pkg = artifacts.packages[row.origin.package];
|
|
153
|
-
const provenance = parseStrictJson(readBytes(join(root, ".oats-installation.json"), "resource-not-found"));
|
|
154
|
-
const expected = { schemaVersion: 1, capability: id, version: row.version, package: row.origin.package,
|
|
155
|
-
packageVersion: pkg.version, source: pkg.source, commit: pkg.commit, packagePath: pkg.path, capabilityPath: row.origin.path };
|
|
156
|
-
for (const [key, value] of Object.entries(expected)) if (provenance[key] !== value) throw oatsError("invalid-resolution", "retained installation provenance differs from its captured rows");
|
|
157
|
-
}
|
|
158
|
-
return manifest;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Verify only exact retained inputs. Does not read today's selection lock,
|
|
162
|
-
* config, catalog, source repository, credential values or approval booleans. */
|
|
163
|
-
export function verifyResolutionInputs(scope, reference, { draft, maxRecords = 256 } = {}) {
|
|
164
|
-
if (!Number.isSafeInteger(maxRecords) || maxRecords < 1 || maxRecords > 4096) throw oatsError("invalid-resolution", "invalid record graph budget");
|
|
165
|
-
const roots = new Map(), records = new Map(), active = new Set();
|
|
166
|
-
let edges = 0, bytes = 0;
|
|
167
|
-
const rootOf = (artifact) => {
|
|
168
|
-
const key = canonicalJson(artifact);
|
|
169
|
-
if (!roots.has(key)) roots.set(key, verifyPortableArtifact(scope, artifact).dir);
|
|
170
|
-
return roots.get(key);
|
|
171
|
-
};
|
|
172
|
-
const verify = (ref, supplied, depth) => {
|
|
173
|
-
validateResolutionRef(ref);
|
|
174
|
-
if (active.has(ref.id)) throw oatsError("invalid-resolution", "captured helper cycle");
|
|
175
|
-
if (records.has(ref.id)) return records.get(ref.id);
|
|
176
|
-
if (depth > 64 || records.size + active.size >= maxRecords) throw oatsError("resource-limit", "captured helper graph limit exceeded");
|
|
177
|
-
const record = supplied ?? readCapturedResolution(scope, ref);
|
|
178
|
-
validateResolutionShape(record);
|
|
179
|
-
const encoded = canonicalJson(record);
|
|
180
|
-
if (jsonIntegrity(record).value !== ref.id) throw oatsError("integrity-drift", "resolution draft/reference mismatch");
|
|
181
|
-
bytes += Buffer.byteLength(encoded);
|
|
182
|
-
if (bytes > 32 * 1024 * 1024) throw oatsError("resource-limit", "aggregate captured record byte limit exceeded");
|
|
183
|
-
active.add(ref.id);
|
|
184
|
-
try {
|
|
185
|
-
if (record.subject.kind === "persistent") {
|
|
186
|
-
const soul = record.subject.soul, root = rootOf(soul.sourceArtifact);
|
|
187
|
-
const definition = resourcePath(root, { path: soul.definition, kind: "file" });
|
|
188
|
-
for (const path of soul.projection.roots) {
|
|
189
|
-
const projected = join(root, path);
|
|
190
|
-
if (!exists(projected)) throw oatsError("resource-not-found", "captured source projection root is absent");
|
|
191
|
-
if (outside(root, realpathSync(projected))) throw oatsError("resource-not-contained", "captured source projection escapes its artifact");
|
|
192
|
-
}
|
|
193
|
-
canonicalSoulFiles(definition);
|
|
194
|
-
verifySoulConstraints(record, root, definition);
|
|
195
|
-
} else resourcePath(rootOf(record.subject.provider), record.subject.definition);
|
|
196
|
-
for (const row of Object.values(record.artifacts.capabilities)) rootOf(row.artifact);
|
|
197
|
-
for (const artifact of record.resourceBundles) rootOf(artifact);
|
|
198
|
-
const resources = new Map();
|
|
199
|
-
for (const [key, resource] of Object.entries(record.resources)) resources.set(key, resourcePath(rootOf(resource.owner), resource));
|
|
200
|
-
const definitions = Object.entries(record.dispatch.providerManifests).map(([id, resourceKey]) => ({
|
|
201
|
-
artifact: record.artifacts.capabilities[id].artifact, bytes: readBytes(resources.get(resourceKey), "resource-not-found"),
|
|
202
|
-
}));
|
|
203
|
-
const manifests = verifyManifestSettings(record, definitions);
|
|
204
|
-
verifyHelperInjectionPolicies(record, definitions);
|
|
205
|
-
for (const [id, manifest] of manifests) {
|
|
206
|
-
const row = record.artifacts.capabilities[id];
|
|
207
|
-
verifyRetainedCapability(rootOf(row.artifact), record.artifacts, id, manifest);
|
|
208
|
-
if (record.subject.kind === "helper" && record.subject.provider.capability === id) {
|
|
209
|
-
const declared = Array.isArray(manifest.agents) && manifest.agents.some((entry) => {
|
|
210
|
-
const path = normalizePackagePath(entry);
|
|
211
|
-
return path !== undefined && record.subject.definition.path === (path === "." ? "soul.yaml" : `${path}/soul.yaml`);
|
|
212
|
-
});
|
|
213
|
-
if (!declared) throw oatsError("resolution-incomplete", "captured helper is not exported by its retained provider");
|
|
214
|
-
const definition = resourcePath(rootOf(row.artifact), record.subject.definition);
|
|
215
|
-
const helper = parseConfigData(readBytes(definition, "resource-not-found")).value;
|
|
216
|
-
if (helper?.name !== record.subject.name) throw oatsError("resolution-incomplete", "captured helper name differs from its definition");
|
|
217
|
-
canonicalSoulFiles(definition);
|
|
218
|
-
}
|
|
219
|
-
for (const [slot, binding] of Object.entries(record.bindings)) {
|
|
220
|
-
if (binding.capability === id && manifest.layer !== slot) throw oatsError("invalid-resolution", "retained provider implements a different fundamental slot");
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
for (const helper of Object.values(record.helpers)) {
|
|
224
|
-
if (++edges > 4096) throw oatsError("resource-limit", "captured helper edge limit exceeded");
|
|
225
|
-
const child = verify(helper, undefined, depth + 1);
|
|
226
|
-
if (child.record.subject.kind !== "helper") throw oatsError("invalid-resolution", "captured helper reference names a persistent soul");
|
|
227
|
-
}
|
|
228
|
-
const result = { record, resources, manifests };
|
|
229
|
-
records.set(ref.id, result);
|
|
230
|
-
return result;
|
|
231
|
-
} finally { active.delete(ref.id); }
|
|
232
|
-
};
|
|
233
|
-
const result = verify(reference, draft, 0);
|
|
234
|
-
return { ...result, reference, roots, records };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Commit only a verified prepared record, by atomic no-replace publication.
|
|
238
|
-
* Legacy reconstruction needs the later explicit migration/evidence verifier. */
|
|
239
|
-
export function commitCapturedResolution(scope, draft) {
|
|
240
|
-
validateResolutionShape(draft);
|
|
241
|
-
if (draft.capture !== "prepared") throw oatsError("migration-required", "historical reconstruction requires the explicit migration verifier");
|
|
242
|
-
const reference = { schemaVersion: 1, id: jsonIntegrity(draft).value };
|
|
243
|
-
const verified = verifyResolutionInputs(scope, reference, { draft });
|
|
244
|
-
let existingRoot;
|
|
245
|
-
try { existingRoot = store(scope, false); }
|
|
246
|
-
catch (error) { if (error.code !== "resolution-not-found") throw error; }
|
|
247
|
-
const existingTarget = existingRoot && join(existingRoot, `${reference.id}.json`);
|
|
248
|
-
if (existingTarget && exists(existingTarget)) { decodeRecord(existingTarget, reference); return reference; }
|
|
249
|
-
// Literal old evidence can be read/reused, but no caller (including callers
|
|
250
|
-
// bypassing the compiler) may mint a new slot-derived/missing-policy helper.
|
|
251
|
-
verifyHelperInjectionPolicies(draft, Object.entries(draft.dispatch.providerManifests).map(([id, key]) => ({
|
|
252
|
-
artifact: draft.artifacts.capabilities[id].artifact, bytes: readBytes(verified.resources.get(key), "resource-not-found"),
|
|
253
|
-
})), { publishing: true });
|
|
254
|
-
const root = store(scope, true), target = join(root, `${reference.id}.json`);
|
|
255
|
-
const staging = mkdtempSync(join(root, ".resolution-"));
|
|
256
|
-
let failed = false, primary;
|
|
257
|
-
try {
|
|
258
|
-
const candidate = join(staging, "record.json");
|
|
259
|
-
writeFileSync(candidate, canonicalJson(draft), { flag: "wx", mode: 0o600 });
|
|
260
|
-
decodeRecord(candidate, reference);
|
|
261
|
-
try { linkSync(candidate, target); } catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
262
|
-
decodeRecord(target, reference);
|
|
263
|
-
return reference;
|
|
264
|
-
} catch (error) { failed = true; primary = error; throw error; }
|
|
265
|
-
finally {
|
|
266
|
-
try { rmSync(staging, { recursive: true, force: true }); }
|
|
267
|
-
catch (cleanup) {
|
|
268
|
-
const error = failed ? new AggregateError([primary, cleanup], "resolution publication and cleanup failed", { cause: primary })
|
|
269
|
-
: new Error("resolution staging cleanup failed", { cause: cleanup });
|
|
270
|
-
error.code = failed ? primary.code : cleanup.code;
|
|
271
|
-
error.stagingPath = staging;
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|