@awebai/oats 0.25.8 → 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 +334 -72
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
- 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 +14 -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-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- 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 +54 -10
- package/docs/design/2026-09-24-phase-d-plan.md +77 -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 +546 -264
- 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 +88 -32
- 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.25.9.md +23 -0
- 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 +7 -7
- 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
|
@@ -1,87 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/** Exact capability-local helper instruction facts. This is a caller of the
|
|
2
|
-
* existing resolver, never a second authority/order or provider-policy engine. */
|
|
3
|
-
import { canonicalJson, parseStrictJson } from './portable-values.mjs';
|
|
4
|
-
import { bytesIntegrity } from './portable-digest.mjs';
|
|
5
|
-
import { pointerKey } from './portable-shape.mjs';
|
|
6
|
-
import { normalizePackagePath } from './capability-provenance.mjs';
|
|
7
|
-
import { resolveChoices } from './portable-choices.mjs';
|
|
8
|
-
import { validateCapabilityInputDeclarations } from './capability-inputs.mjs';
|
|
9
|
-
import { oatsError } from './errors.mjs';
|
|
10
|
-
|
|
11
|
-
const same = (a, b) => canonicalJson(a) === canonicalJson(b);
|
|
12
|
-
const prefix = '/helpers/injections/';
|
|
13
|
-
export const helperInjectionChoiceKey = id => `${prefix}${pointerKey(id)}`;
|
|
14
|
-
|
|
15
|
-
/** artifact/bytes are exact selected manifest inputs, not annotated objects. */
|
|
16
|
-
export function helperInjectionFact({ artifact, bytes }) {
|
|
17
|
-
const manifest = validateCapabilityInputDeclarations(parseStrictJson(bytes));
|
|
18
|
-
if (artifact.kind !== 'capability' || manifest.capability !== artifact.capability) throw oatsError('invalid-resolution', 'helper policy manifest owner differs from selected artifact');
|
|
19
|
-
if (!Object.hasOwn(manifest, 'helperInjection')) return { manifest, fact: null, resource: null };
|
|
20
|
-
const policy = manifest.helperInjection, source = `capability:${artifact.capability}`, key = helperInjectionChoiceKey(artifact.capability);
|
|
21
|
-
let path = null;
|
|
22
|
-
if (policy.mode === 'inherit') {
|
|
23
|
-
path = normalizePackagePath(manifest.inject);
|
|
24
|
-
if (typeof manifest.inject !== 'string' || path === undefined || path === '.') throw oatsError('needs-configuration', 'helper inherit needs its own declared injection file');
|
|
25
|
-
} else if (policy.mode === 'file') path = policy.path;
|
|
26
|
-
const resourceKey = `instruction:${source}`;
|
|
27
|
-
const origin = { kind: 'source-export', document: { kind: 'artifact', owner: artifact, path: 'oats.json', integrity: bytesIntegrity(bytes) }, pointer: '/helperInjection' };
|
|
28
|
-
return { manifest, source, resourceKey, resource: path === null ? null : { owner: artifact, path, kind: 'file' },
|
|
29
|
-
fact: { key, kind: 'equals', value: path === null ? null : resourceKey, origin } };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function captureHelperInjectionChoices(plan, definitions) {
|
|
33
|
-
const policies = new Map(), requirements = [...plan.requirements];
|
|
34
|
-
for (const definition of definitions) {
|
|
35
|
-
const policy = helperInjectionFact(definition), id = definition.artifact.capability;
|
|
36
|
-
if (policies.has(id)) throw oatsError('invalid-resolution', 'duplicate helper policy owner');
|
|
37
|
-
policies.set(id, policy);
|
|
38
|
-
if (!policy.fact && policy.manifest.inject) {
|
|
39
|
-
// Attributed like every other preparation problem: the operator learns WHICH
|
|
40
|
-
// capability lacks the declaration, not only that one does (second-operator finding).
|
|
41
|
-
const error = oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
|
|
42
|
-
error.problems = [{ code: 'needs-configuration', message: 'capability ships an inject without a helperInjection policy', capability: id, ...(policy.manifest.layer ? { slot: policy.manifest.layer } : {}) }];
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
if (policy.fact) requirements.push(policy.fact);
|
|
46
|
-
}
|
|
47
|
-
const resolved = resolveChoices({ requirements, candidates: plan.candidates });
|
|
48
|
-
if (resolved.status !== 'resolved') throw oatsError(resolved.status === 'conflict' ? 'requirement-conflict' : 'needs-configuration', 'helper policy conflicts with captured instruction choices', resolved.problems);
|
|
49
|
-
return { choices: resolved.choices, policies };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Verify BOTH directions. Read-only verification permits literal legacy
|
|
53
|
-
* evidence; publication must explicitly disallow new legacy/missing-policy mint.
|
|
54
|
-
* Existing resource verification still proves physical containment/kind. */
|
|
55
|
-
export function verifyHelperInjectionPolicies(record, definitions, { publishing = false } = {}) {
|
|
56
|
-
const helper = record.subject.kind === 'helper', composition = record.dispatch.composition;
|
|
57
|
-
const expected = new Map(), selected = new Set();
|
|
58
|
-
for (const definition of definitions) {
|
|
59
|
-
const policy = helperInjectionFact(definition), id = definition.artifact.capability;
|
|
60
|
-
if (selected.has(id) || !record.artifacts.capabilities[id] || !same(record.artifacts.capabilities[id].artifact, definition.artifact)) throw oatsError('invalid-resolution', 'helper policy inputs differ from selected artifacts');
|
|
61
|
-
selected.add(id);
|
|
62
|
-
if (!helper) continue;
|
|
63
|
-
if (publishing && !policy.fact && policy.manifest.inject) throw oatsError('needs-configuration', 'new helper record lacks an explicit injection policy');
|
|
64
|
-
if (!policy.fact) continue;
|
|
65
|
-
const { fact, source, resource, resourceKey } = policy, choice = record.choices[fact.key];
|
|
66
|
-
expected.set(fact.key, fact);
|
|
67
|
-
if (!choice || !same(choice.value, fact.value) || !choice.constraints.some(entry => entry.kind === fact.kind && same(entry.value, fact.value) && same(entry.origin, fact.origin))) throw oatsError('resolution-incomplete', 'helper injection lacks its exact retained manifest witness');
|
|
68
|
-
const block = composition?.blocks.find(entry => entry.source === source), omission = composition?.omissions.find(entry => entry.source === source);
|
|
69
|
-
if (resource) {
|
|
70
|
-
if (omission || !block || block.choice !== fact.key || block.resource !== resourceKey || !record.resources[resourceKey] || !same(record.resources[resourceKey], resource)) throw oatsError('invalid-resolution', 'helper instruction block differs from declared owner/file');
|
|
71
|
-
} else if (block || !omission || omission.reason !== 'helper-policy' || omission.choice !== fact.key) throw oatsError('invalid-resolution', 'helper omission differs from explicit capability policy');
|
|
72
|
-
}
|
|
73
|
-
if (selected.size !== Object.keys(record.artifacts.capabilities).length) throw oatsError('resolution-incomplete', 'helper policy manifest inputs are incomplete');
|
|
74
|
-
const isPolicyOrigin = origin => origin?.kind === 'source-export' && (origin.pointer === '/helperInjection' || origin.pointer?.startsWith('/helperInjection/'));
|
|
75
|
-
for (const [key, choice] of Object.entries(record.choices)) {
|
|
76
|
-
if (key.startsWith(prefix) && !expected.has(key)) throw oatsError('invalid-resolution', 'helper policy choice lacks an applicable declaration');
|
|
77
|
-
for (const entry of [...choice.constraints, ...choice.considered]) if (isPolicyOrigin(entry.origin)) {
|
|
78
|
-
const fact = expected.get(key);
|
|
79
|
-
if (!fact || entry.kind !== 'equals' || !same(entry.value, fact.value) || !same(entry.origin, fact.origin)) throw oatsError('invalid-resolution', 'helper manifest witness cannot justify another choice or owner');
|
|
80
|
-
}
|
|
81
|
-
if (isPolicyOrigin(choice.selectedBy) && (!expected.has(key) || !same(choice.selectedBy, expected.get(key).origin))) throw oatsError('invalid-resolution', 'helper selected origin differs from its declaration');
|
|
82
|
-
}
|
|
83
|
-
const ownSource = fact => `capability:${fact.origin.document.owner.capability}`;
|
|
84
|
-
for (const block of composition?.blocks ?? []) if (block.choice?.startsWith(prefix)) {
|
|
85
|
-
const fact = expected.get(block.choice);
|
|
86
|
-
if (!fact || fact.value === null || block.source !== ownSource(fact) || block.resource !== fact.value) throw oatsError('invalid-resolution', 'helper instruction witness cannot control another contribution');
|
|
87
|
-
}
|
|
88
|
-
for (const omission of composition?.omissions ?? []) {
|
|
89
|
-
if (publishing && omission.reason === 'helper-knowledge') throw oatsError('needs-configuration', 'new publication cannot mint legacy slot-based helper omissions');
|
|
90
|
-
if (omission.reason === 'helper-policy') {
|
|
91
|
-
const fact = expected.get(omission.choice);
|
|
92
|
-
if (!helper || !fact || fact.value !== null || omission.source !== ownSource(fact)) throw oatsError('invalid-resolution', 'helper omission lacks its declared own policy');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// A missing/non-policy choice is not a way around capability ownership.
|
|
96
|
-
// Above we prove each declared contribution's exact file/omission; here every
|
|
97
|
-
// new capability contribution must point back to that owner's proved fact.
|
|
98
|
-
// Literal historical read/reuse and primary composition stay unchanged.
|
|
99
|
-
if (helper && publishing) for (const entry of [...(composition?.blocks ?? []), ...(composition?.omissions ?? [])]) {
|
|
100
|
-
if (!entry.source.startsWith('capability:')) continue;
|
|
101
|
-
const fact = expected.get(helperInjectionChoiceKey(entry.source.slice('capability:'.length)));
|
|
102
|
-
if (!fact || entry.choice !== fact.key) throw oatsError('invalid-resolution', 'new helper capability contribution lacks its declared own policy');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/** Sole bounded structural decoder for historical lockfileVersion 1 and the
|
|
2
|
-
* capability-materialization lockfileVersion 2.
|
|
3
|
-
*
|
|
4
|
-
* Bytes in, validated null-prototype maps out. No filesystem/config reads,
|
|
5
|
-
* migration, normalization, repair or writes. Retired-capability policy is an
|
|
6
|
-
* injected pure predicate so this leaf does not import the kernel. */
|
|
7
|
-
import { byteView, parseStrictJson } from "./portable-values.mjs";
|
|
8
|
-
import {
|
|
9
|
-
PACKAGE_ID_RE, TRANSITIONAL_ROW_FIELDS, capabilityIdViolation,
|
|
10
|
-
isMaterializedCapabilityId, validateCapabilityLockEntry, validateLockEntry,
|
|
11
|
-
} from "./capability-provenance.mjs";
|
|
12
|
-
import { oatsError } from "./errors.mjs";
|
|
13
|
-
|
|
14
|
-
const LOCK_PACKAGE_KEYS = new Set(["source", "path", "version", "commit", "integrity", "dependencies"]);
|
|
15
|
-
const LOCK_CAPABILITY_KEYS = new Set(["version", "package", "path", "integrity", "trusted"]);
|
|
16
|
-
const noneRetired = () => undefined;
|
|
17
|
-
const nullProtoMap = (raw) => {
|
|
18
|
-
const out = Object.create(null);
|
|
19
|
-
for (const key of Object.keys(raw || {})) out[key] = raw[key];
|
|
20
|
-
return out;
|
|
21
|
-
};
|
|
22
|
-
const kind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
23
|
-
|
|
24
|
-
/** Validate one literal v1 capability row. Unknown historical fields remain
|
|
25
|
-
* preserved, matching the existing reader; required fields and known optional
|
|
26
|
-
* field types remain strict. */
|
|
27
|
-
export function legacyCapabilityEntryViolation(entry) {
|
|
28
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return "not an object";
|
|
29
|
-
for (const key of ["source", "version", "integrity"]) if (typeof entry[key] !== "string" || !entry[key]) return `missing/invalid ${key}`;
|
|
30
|
-
if (!/^sha256-[0-9a-f]{64}$/.test(entry.integrity)) return `malformed integrity "${entry.integrity}"`;
|
|
31
|
-
if (entry.commit !== undefined && typeof entry.commit !== "string") return "invalid commit";
|
|
32
|
-
if (entry.trustedExecutables !== undefined && typeof entry.trustedExecutables !== "boolean") return "invalid trustedExecutables";
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Decode one already-read historical lock. `file` is diagnostic provenance,
|
|
37
|
-
* not a path this codec reads. Strict JSON adds bounded/duplicate-key/UTF-8
|
|
38
|
-
* refusal before the unchanged v1/v2 semantic pass. */
|
|
39
|
-
export function decodeLegacyLockBytes(input, { file = "<legacy-lock>", retiredCapabilityReason = noneRetired, limits } = {}) {
|
|
40
|
-
const bytes = byteView(input, "legacy lock bytes");
|
|
41
|
-
if (typeof file !== "string" || !file) throw oatsError("invalid-lock", "legacy lock diagnostic file must be non-empty text");
|
|
42
|
-
if (typeof retiredCapabilityReason !== "function") throw oatsError("invalid-lock", `${file}: retired-capability classifier must be a function`);
|
|
43
|
-
const bad = (message, extra = {}) => oatsError("invalid-lock", `${file}: ${message}`, [{ file, violation: message, ...extra }]);
|
|
44
|
-
let parsed;
|
|
45
|
-
try { parsed = parseStrictJson(bytes, limits); }
|
|
46
|
-
catch (error) { const failure = bad(`malformed JSON — ${error.message}`); failure.cause = error; throw failure; }
|
|
47
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw bad(`lock root must be a JSON object (got ${kind(parsed)})`);
|
|
48
|
-
const rawVersion = parsed.lockfileVersion;
|
|
49
|
-
if (rawVersion !== undefined && typeof rawVersion !== "number") throw bad(`lockfileVersion must be a number (got ${JSON.stringify(rawVersion)})`);
|
|
50
|
-
if (rawVersion !== undefined && rawVersion !== 1 && rawVersion !== 2) throw bad(`unsupported lockfileVersion ${rawVersion}`);
|
|
51
|
-
const version = rawVersion ?? 1;
|
|
52
|
-
if (parsed.capabilities !== undefined && (!parsed.capabilities || typeof parsed.capabilities !== "object" || Array.isArray(parsed.capabilities))) {
|
|
53
|
-
throw bad(`"capabilities" must be an object map (got ${kind(parsed.capabilities)})`);
|
|
54
|
-
}
|
|
55
|
-
const out = { version, packages: Object.create(null), capabilities: Object.create(null), legacyCapabilities: Object.create(null) };
|
|
56
|
-
if (version === 1) {
|
|
57
|
-
if (parsed.packages !== undefined) throw bad(`lockfileVersion 1 must not carry a "packages" map`);
|
|
58
|
-
for (const id of Object.keys(parsed.capabilities || {})) {
|
|
59
|
-
const entry = parsed.capabilities[id];
|
|
60
|
-
if (!retiredCapabilityReason(id)) {
|
|
61
|
-
const violation = legacyCapabilityEntryViolation(entry);
|
|
62
|
-
if (violation) throw bad(`legacy entry "${id}" is malformed (${violation})`, { package: id });
|
|
63
|
-
}
|
|
64
|
-
out.legacyCapabilities[id] = entry;
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const rawPackages = parsed.packages;
|
|
70
|
-
if (!rawPackages || typeof rawPackages !== "object" || Array.isArray(rawPackages)) {
|
|
71
|
-
throw bad(`lockfileVersion 2 requires a "packages" object map (got ${kind(rawPackages)})`);
|
|
72
|
-
}
|
|
73
|
-
const packageKeys = Object.keys(rawPackages), hasCapabilityMap = Object.hasOwn(parsed, "capabilities");
|
|
74
|
-
const stateFree = packageKeys.length === 0 && (!hasCapabilityMap || Object.keys(parsed.capabilities).length === 0);
|
|
75
|
-
const unsupported = (why) => bad(`unsupported transitional package-root lockfileVersion 2 (${why}). This is the superseded package-store lock shape; it is not converted or interpreted. Delete this lock (and any .agents/packages directory) and recreate the scope's state with \`oats install\`.`);
|
|
76
|
-
if (!stateFree) {
|
|
77
|
-
if (!hasCapabilityMap) throw unsupported(`no top-level "capabilities" map`);
|
|
78
|
-
for (const id of packageKeys) {
|
|
79
|
-
const row = rawPackages[id];
|
|
80
|
-
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
81
|
-
const tells = TRANSITIONAL_ROW_FIELDS.filter((field) => Object.hasOwn(row, field));
|
|
82
|
-
if (tells.length) throw unsupported(`package row "${id}" carries ${tells.join(", ")}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
for (const id of packageKeys) {
|
|
86
|
-
if (!PACKAGE_ID_RE.test(id)) throw bad(`packages map has an invalid package key ${JSON.stringify(id)}`, { package: id });
|
|
87
|
-
const entry = rawPackages[id];
|
|
88
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw bad(`lock entry for "${id}" is not an object`, { package: id });
|
|
89
|
-
const extra = Object.keys(entry).filter((key) => !LOCK_PACKAGE_KEYS.has(key));
|
|
90
|
-
if (extra.length) throw bad(`lock entry for "${id}" has unknown keys: ${extra.join(", ")}`, { package: id });
|
|
91
|
-
out.packages[id] = entry;
|
|
92
|
-
}
|
|
93
|
-
for (const id of Object.keys(out.packages)) validateLockEntry(id, out.packages[id], out.packages, { file });
|
|
94
|
-
if (hasCapabilityMap) {
|
|
95
|
-
for (const id of Object.keys(parsed.capabilities)) {
|
|
96
|
-
if (!isMaterializedCapabilityId(id)) throw bad(`capabilities map has an invalid capability key: ${capabilityIdViolation(id)}`);
|
|
97
|
-
const entry = parsed.capabilities[id];
|
|
98
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw bad(`capability lock entry for "${id}" is not an object`, { package: id });
|
|
99
|
-
const extra = Object.keys(entry).filter((key) => !LOCK_CAPABILITY_KEYS.has(key));
|
|
100
|
-
if (extra.length) throw bad(`capability lock entry for "${id}" has unknown keys: ${extra.join(", ")}`, { package: id });
|
|
101
|
-
out.capabilities[id] = entry;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
for (const id of Object.keys(out.capabilities)) validateCapabilityLockEntry(id, out.capabilities[id], out.packages, { file });
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/** Manifest-owned setting defaults are intrinsic field fallbacks, not another
|
|
2
|
-
* workspace/repository policy authority. Capture them before record publication. */
|
|
3
|
-
import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
|
|
4
|
-
import { bytesIntegrity } from "./portable-digest.mjs";
|
|
5
|
-
import { objectAt, pointerKey } from "./portable-shape.mjs";
|
|
6
|
-
import { validateArtifactRef, validateOrigin } from "./resolution-shape.mjs";
|
|
7
|
-
import { settingChoiceKey } from "./soul-constraints.mjs";
|
|
8
|
-
import { resolveChoices } from "./portable-choices.mjs";
|
|
9
|
-
import { oatsError } from "./errors.mjs";
|
|
10
|
-
|
|
11
|
-
export function manifestSettingDefaults(artifact, bytes) {
|
|
12
|
-
validateArtifactRef(artifact);
|
|
13
|
-
if (artifact.kind !== "capability") throw oatsError("invalid-declaration", "manifest defaults need a capability artifact owner");
|
|
14
|
-
const manifest = parseStrictJson(bytes);
|
|
15
|
-
objectAt(manifest, null, ["capability", "version"]);
|
|
16
|
-
if (manifest.capability !== artifact.capability) throw oatsError("invalid-resolution", "manifest default owner differs from its artifact");
|
|
17
|
-
const defaults = Object.create(null);
|
|
18
|
-
if (manifest.settings !== undefined) objectAt(manifest.settings, null, [], "/settings");
|
|
19
|
-
const document = { kind: "artifact", owner: artifact, path: "oats.json", integrity: bytesIntegrity(bytes) };
|
|
20
|
-
for (const [name, declaration] of Object.entries(manifest.settings ?? {})) {
|
|
21
|
-
if (!declaration || typeof declaration !== "object" || Array.isArray(declaration) || !Object.hasOwn(declaration, "default")) continue;
|
|
22
|
-
const origin = { kind: "manifest-default", document, pointer: `/settings/${pointerKey(name)}/default` };
|
|
23
|
-
validateOrigin(origin);
|
|
24
|
-
defaults[name] = { key: settingChoiceKey(artifact.capability, name), kind: "manifest-default", value: declaration.default, origin };
|
|
25
|
-
}
|
|
26
|
-
return { manifest, defaults };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Definitions are the selected, verified manifest byte snapshots. No provider
|
|
30
|
-
* payload code runs here, and no source/lock/approval lookup is performed. */
|
|
31
|
-
export function captureManifestSettings(plan, definitions) {
|
|
32
|
-
canonicalJson(plan);
|
|
33
|
-
if (plan.status === "conflict") throw oatsError("requirement-conflict", "cannot capture defaults for a conflicting software plan");
|
|
34
|
-
if (!Array.isArray(definitions)) throw oatsError("invalid-declaration", "selected manifest definitions must be an array");
|
|
35
|
-
const candidates = [...plan.candidates], settings = Object.create(null), seen = new Set();
|
|
36
|
-
for (const id of Object.keys(plan.capabilities)) settings[id] = Object.assign(Object.create(null), plan.settings[id] ?? {});
|
|
37
|
-
for (const { artifact, bytes } of definitions) {
|
|
38
|
-
const { defaults } = manifestSettingDefaults(artifact, bytes), id = artifact.capability;
|
|
39
|
-
if (!Object.hasOwn(plan.capabilities, id) || seen.has(id)) throw oatsError("invalid-resolution", "manifest defaults do not match the selected capability set");
|
|
40
|
-
seen.add(id);
|
|
41
|
-
for (const [name, candidate] of Object.entries(defaults)) {
|
|
42
|
-
candidates.push(candidate); settings[id][name] = candidate.key;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (seen.size !== Object.keys(plan.capabilities).length) throw oatsError("resolution-incomplete", "selected manifest defaults were not fully captured");
|
|
46
|
-
const resolved = resolveChoices({ requirements: plan.requirements, candidates });
|
|
47
|
-
return { ...plan, ...resolved, candidates, settings };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Check both directions over ALL choices, not only settings the dispatch map
|
|
51
|
-
* happens to enumerate. Neither omissions nor invented intrinsic facts are valid,
|
|
52
|
-
* including overridden facts. Read each selected manifest only once. */
|
|
53
|
-
export function verifyManifestSettings(record, definitions) {
|
|
54
|
-
const manifests = new Map(), expected = new Map();
|
|
55
|
-
const originKey = ({ kind, document, pointer }) => canonicalJson({ kind, document, pointer });
|
|
56
|
-
const matches = (entry, candidate) => candidate && entry.kind === "manifest-default"
|
|
57
|
-
&& canonicalJson(entry.value) === canonicalJson(candidate.value) && originKey(entry.origin) === originKey(candidate.origin);
|
|
58
|
-
for (const { artifact, bytes } of definitions) {
|
|
59
|
-
const { manifest, defaults } = manifestSettingDefaults(artifact, bytes), id = artifact.capability;
|
|
60
|
-
const selected = record.artifacts.capabilities[id];
|
|
61
|
-
if (!selected || manifests.has(id) || canonicalJson(selected.artifact) !== canonicalJson(artifact)) {
|
|
62
|
-
throw oatsError("resolution-incomplete", "default verification does not match the selected capability set");
|
|
63
|
-
}
|
|
64
|
-
manifests.set(id, manifest);
|
|
65
|
-
for (const [name, candidate] of Object.entries(defaults)) {
|
|
66
|
-
expected.set(candidate.key, candidate);
|
|
67
|
-
const key = record.dispatch.settingsChoices[id]?.[name], choice = record.choices[candidate.key];
|
|
68
|
-
if (key !== candidate.key || !choice || !choice.considered.some((entry) => matches(entry, candidate))) {
|
|
69
|
-
throw oatsError("resolution-incomplete", "captured setting omits its exact manifest default or provenance");
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (manifests.size !== Object.keys(record.artifacts.capabilities).length) {
|
|
74
|
-
throw oatsError("resolution-incomplete", "selected manifest defaults were not fully verified");
|
|
75
|
-
}
|
|
76
|
-
for (const [key, choice] of Object.entries(record.choices)) {
|
|
77
|
-
for (const entry of choice.considered) {
|
|
78
|
-
if (entry.kind === "manifest-default" && !matches(entry, expected.get(key))) {
|
|
79
|
-
throw oatsError("resolution-incomplete", "captured intrinsic default is not witnessed by its selected manifest");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return manifests;
|
|
84
|
-
}
|
package/lib/package-closure.mjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/** One bounded staged dependency walker. Adapters own source syntax/fetch,
|
|
2
|
-
* manifest compatibility and digest versions; this engine owns graph identity,
|
|
3
|
-
* cycle checks and ordering, never installation, selection or executable trust. */
|
|
4
|
-
import { oatsError } from "./errors.mjs";
|
|
5
|
-
|
|
6
|
-
export function resolvePackageClosure({ requests, readPackage, validatePackage, finalizePackage, discardPackage,
|
|
7
|
-
context = "package preparation", maxPackages = 256, maxDepth = 64, maxRequests = 1024 }) {
|
|
8
|
-
if (!Array.isArray(requests) || !requests.length) throw oatsError("invalid-source", "package closure requires explicit root requests");
|
|
9
|
-
for (const callback of [readPackage, validatePackage, finalizePackage, discardPackage]) {
|
|
10
|
-
if (typeof callback !== "function") throw new TypeError("package closure requires complete source/validation/ownership adapters");
|
|
11
|
-
}
|
|
12
|
-
for (const limit of [maxPackages, maxDepth, maxRequests]) {
|
|
13
|
-
if (!Number.isSafeInteger(limit) || limit < 1) throw oatsError("resource-limit", "invalid package graph budget");
|
|
14
|
-
}
|
|
15
|
-
if (maxDepth > 256) throw oatsError("resource-limit", "package graph depth budget exceeds supported traversal");
|
|
16
|
-
const resolved = new Map();
|
|
17
|
-
let reads = 0;
|
|
18
|
-
const visit = (request, parent, chain) => {
|
|
19
|
-
if (++reads > maxRequests || chain.length > maxDepth) {
|
|
20
|
-
throw oatsError("resource-limit", "package dependency graph budget exceeded");
|
|
21
|
-
}
|
|
22
|
-
const record = readPackage(request, parent, chain);
|
|
23
|
-
if (!record || typeof record.package !== "string" || !record.package || typeof record.sourceKey !== "string" || !record.sourceKey
|
|
24
|
-
|| !Array.isArray(record.dependencyRequests)) throw oatsError("invalid-package-manifest", "package source adapter returned incomplete graph data");
|
|
25
|
-
const id = record.package;
|
|
26
|
-
if (chain.includes(id)) throw oatsError("dependency-cycle", `package dependency cycle: ${[...chain, id].join(" → ")}`, [...chain, id]);
|
|
27
|
-
if (resolved.has(id)) {
|
|
28
|
-
const previous = resolved.get(id);
|
|
29
|
-
if (previous.sourceKey !== record.sourceKey) {
|
|
30
|
-
throw oatsError("duplicate-package-identity", `two sources claim package "${id}" at ${context}: ${previous.sourceKey} and ${record.sourceKey}`,
|
|
31
|
-
[...(previous.origins ?? [previous.sourceKey]), ...(record.origins ?? [record.sourceKey])]);
|
|
32
|
-
}
|
|
33
|
-
// A source adapter may reuse an already staged root. Never discard the
|
|
34
|
-
// authoritative copy merely because another edge reached it again.
|
|
35
|
-
if (record.dir !== previous.dir) discardPackage(record, previous);
|
|
36
|
-
return id;
|
|
37
|
-
}
|
|
38
|
-
if (resolved.size + chain.length >= maxPackages) throw oatsError("resource-limit", "package identity budget exceeded");
|
|
39
|
-
validatePackage(record);
|
|
40
|
-
const deps = record.dependencyRequests.map((dependency) => visit(dependency, record, [...chain, id]));
|
|
41
|
-
const complete = finalizePackage(record, deps);
|
|
42
|
-
if (!complete || complete.package !== id || complete.sourceKey !== record.sourceKey) throw oatsError("invalid-package-manifest", "package finalizer changed graph identity");
|
|
43
|
-
resolved.set(id, complete);
|
|
44
|
-
return id;
|
|
45
|
-
};
|
|
46
|
-
const roots = requests.map((request) => visit(request, null, []));
|
|
47
|
-
return { roots, packages: resolved };
|
|
48
|
-
}
|