@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.
Files changed (177) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +576 -1714
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +8 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-review/oats.json +3 -2
  20. package/docs/capabilities.md +218 -47
  21. package/docs/capability-manifest.schema.json +13 -4
  22. package/docs/configuration.md +17 -5
  23. package/docs/conventions.md +16 -26
  24. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  25. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  26. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  27. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  28. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  29. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  30. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  31. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  32. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
  33. package/docs/design/2026-09-24-phase-d-plan.md +57 -0
  34. package/docs/design/2026-09-25-teams-contract.md +226 -0
  35. package/docs/design/README.md +3 -3
  36. package/docs/design/launch-configurations.md +20 -16
  37. package/docs/design/operations-contract.md +27 -10
  38. package/docs/desktop-cli-api.md +537 -261
  39. package/docs/desktop-instance-start.md +1 -1
  40. package/docs/desktop.md +7 -13
  41. package/docs/execution-targets.md +16 -18
  42. package/docs/first-team.md +14 -17
  43. package/docs/implementation.md +28 -59
  44. package/docs/integrations.md +64 -33
  45. package/docs/knowledge-capability-authoring.md +1 -1
  46. package/docs/knowledge-reference/package-craft.md +10 -8
  47. package/docs/knowledge-theory.md +1 -1
  48. package/docs/knowledge.md +10 -11
  49. package/docs/layers.md +16 -17
  50. package/docs/oats-local.schema.json +29 -1
  51. package/docs/oats-membership.schema.json +5 -3
  52. package/docs/oats-package.schema.json +2 -2
  53. package/docs/oats-workspace.schema.json +1 -1
  54. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  55. package/docs/packages.md +75 -52
  56. package/docs/release-notes/v0.22.0.md +1 -1
  57. package/docs/release-notes/v0.23.1.md +1 -1
  58. package/docs/release-notes/v0.26.0.md +670 -0
  59. package/docs/schedules.md +48 -126
  60. package/docs/soul.schema.json +11 -4
  61. package/docs/souls-and-instances.md +56 -43
  62. package/docs/workspaces.md +80 -58
  63. package/injects/instance-boundary.md +1 -1
  64. package/injects/work-attached.md +1 -1
  65. package/injects/work-workspace.md +2 -2
  66. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  67. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  68. package/lib/capability-contract.mjs +110 -0
  69. package/lib/config-data.mjs +2 -2
  70. package/lib/core.mjs +700 -4824
  71. package/lib/digest.mjs +12 -0
  72. package/lib/instance-inspect.mjs +396 -0
  73. package/lib/instance-lifecycle.mjs +3 -4
  74. package/lib/instance-resolution.mjs +212 -26
  75. package/lib/instruction-composition.mjs +0 -20
  76. package/lib/materialize.mjs +6 -4
  77. package/lib/operator-dispatch.mjs +33 -13
  78. package/lib/packages.mjs +25 -190
  79. package/lib/provider-binding.mjs +4 -2
  80. package/lib/provider-reasons.mjs +3 -68
  81. package/lib/resolve.mjs +204 -68
  82. package/lib/schedule.mjs +97 -272
  83. package/lib/servers.mjs +13 -13
  84. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  85. package/lib/tree-copy.mjs +44 -0
  86. package/lib/workspace.mjs +125 -20
  87. package/package-catalog.json +6 -6
  88. package/package.json +1 -1
  89. package/skills/integration-authoring/SKILL.md +48 -40
  90. package/skills/oats-getting-started/SKILL.md +105 -110
  91. package/skills/oats-support/SKILL.md +2 -2
  92. package/skills/soul-craft/SKILL.md +13 -6
  93. package/bin/oats-pi-sdk-host.mjs +0 -17
  94. package/docs/2026-09-03-architecture-proposal.md +0 -642
  95. package/docs/artifact-approvals.schema.json +0 -7
  96. package/docs/captured-invocation-context.schema.json +0 -7
  97. package/docs/captured-resolution.schema.json +0 -7
  98. package/docs/design/package-engine-contract.md +0 -813
  99. package/docs/design/package-runtime-api.md +0 -588
  100. package/docs/desktop-succession.md +0 -57
  101. package/docs/execution-capsule.schema.json +0 -108
  102. package/docs/first-team-demo.md +0 -92
  103. package/docs/knowledge-migration.md +0 -147
  104. package/docs/migration-from-oas.md +0 -103
  105. package/docs/oats-config.schema.json +0 -172
  106. package/docs/oats-lock-v3.schema.json +0 -7
  107. package/docs/oats-lock.schema.json +0 -175
  108. package/docs/operating-team-migration.md +0 -470
  109. package/docs/portable.schema.json +0 -2512
  110. package/docs/provider-check-input.schema.json +0 -7
  111. package/docs/rebuild-to-v2.md +0 -511
  112. package/docs/workspace-adoption.md +0 -74
  113. package/injects/framework-workspace.md +0 -7
  114. package/injects/local-soul.md +0 -19
  115. package/injects/oats-portable.md +0 -20
  116. package/injects/oats.md +0 -11
  117. package/injects/portable-instance-boundary.md +0 -39
  118. package/injects/portable-work-directory.md +0 -29
  119. package/lib/artifact-approvals.mjs +0 -120
  120. package/lib/artifact-tree.mjs +0 -141
  121. package/lib/capability-artifacts.mjs +0 -179
  122. package/lib/capability-execution.mjs +0 -15
  123. package/lib/capability-inputs.mjs +0 -39
  124. package/lib/capability-provenance.mjs +0 -231
  125. package/lib/captured-action-shape.mjs +0 -21
  126. package/lib/captured-admission-shape.mjs +0 -20
  127. package/lib/captured-binding-file.mjs +0 -36
  128. package/lib/captured-dispatch.mjs +0 -66
  129. package/lib/captured-instance-index.mjs +0 -277
  130. package/lib/captured-invocation-context.mjs +0 -130
  131. package/lib/captured-launch-request.mjs +0 -66
  132. package/lib/captured-operation-process.mjs +0 -15
  133. package/lib/captured-pi-custody.mjs +0 -29
  134. package/lib/captured-pi-host.mjs +0 -167
  135. package/lib/captured-pi-outcome.mjs +0 -172
  136. package/lib/captured-resolutions.mjs +0 -275
  137. package/lib/captured-scaffold.mjs +0 -87
  138. package/lib/captured-selector.mjs +0 -28
  139. package/lib/captured-session-backend.mjs +0 -52
  140. package/lib/captured-source-receipt-file.mjs +0 -72
  141. package/lib/helper-injection-policy.mjs +0 -104
  142. package/lib/legacy-lock-codec.mjs +0 -106
  143. package/lib/manifest-settings.mjs +0 -84
  144. package/lib/package-closure.mjs +0 -48
  145. package/lib/package-materialization.mjs +0 -83
  146. package/lib/pi-sdk-host.mjs +0 -229
  147. package/lib/portable-artifacts.mjs +0 -115
  148. package/lib/portable-choices.mjs +0 -82
  149. package/lib/portable-composition.mjs +0 -136
  150. package/lib/portable-digest.mjs +0 -105
  151. package/lib/portable-identity.mjs +0 -40
  152. package/lib/portable-lock.mjs +0 -117
  153. package/lib/portable-onboarding-request.mjs +0 -49
  154. package/lib/portable-onboarding.mjs +0 -256
  155. package/lib/portable-package-preparation.mjs +0 -188
  156. package/lib/portable-policy.mjs +0 -44
  157. package/lib/portable-soul.mjs +0 -42
  158. package/lib/portable-state.mjs +0 -80
  159. package/lib/prepare-composition.mjs +0 -170
  160. package/lib/prepared-bindings.mjs +0 -92
  161. package/lib/prepared-resources.mjs +0 -127
  162. package/lib/provider-binding-broker.mjs +0 -65
  163. package/lib/provider-binding-wire.mjs +0 -116
  164. package/lib/readiness.mjs +0 -225
  165. package/lib/repository-observation.mjs +0 -226
  166. package/lib/resolution-shape.mjs +0 -393
  167. package/lib/schedule-capsule.mjs +0 -206
  168. package/lib/soul-constraints.mjs +0 -40
  169. package/lib/source-projection.mjs +0 -84
  170. package/lib/source-spec.mjs +0 -189
  171. package/lib/workspace-definition.mjs +0 -126
  172. package/lib/workspace-discovery.mjs +0 -146
  173. package/skills/oats/SKILL.md +0 -162
  174. package/skills/oats-config/SKILL.md +0 -164
  175. package/skills/oats-packages/SKILL.md +0 -184
  176. package/skills/oats-portable/SKILL.md +0 -115
  177. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -1,39 +0,0 @@
1
- ## Captured instance: home, source and work
2
-
3
- **Your instance home** is the specific OATS instance directory supplied as
4
- `OATS_INSTANCE_HOME`, not your user home, source repository or work target.
5
- `instance.json.executionBinding` names the explicit captured **deployment and
6
- resolution**. Home-bound actions must also match this owned home's incarnation
7
- and recorded custody. Do not invent, copy or rewrite those identifiers to make
8
- another home or composition appear authorized.
9
-
10
- - **Home holds composed instructions and instance state.** `AGENTS.md` is the
11
- canonical composed instruction file; `CLAUDE.md -> AGENTS.md` is its relative
12
- compatibility alias, not a second instruction source. Preserve the generated
13
- instructions, aliases and metadata; do not hand-edit them to change authority.
14
- Task material and provider-managed state belong where their owning contract
15
- specifies. The selected capabilities define any knowledge or memory protocol.
16
- - **`./soul` is a read-only retained source link, not your edit surface.** Reading
17
- it must not depend on the publisher's current checkout. Never write through it
18
- or modify retained artifacts. If a task authorizes source changes, use its
19
- explicitly authorized tracked work surface and review path instead.
20
- - **`./work` is the task's work surface.** The work-mode instructions determine
21
- whether it is an owned directory or another permitted repository view. Make
22
- task edits only on that authorized surface, not in deployment stores or a
23
- convenient source checkout. Reading an external input is not permission to
24
- modify it or its owner.
25
-
26
- For supported captured commands, keep the explicit `--deployment` and
27
- `--resolution` pair from the recorded binding; supply the exact owned home when
28
- an action requires one. Running from home preserves the invocation's working
29
- location, but **cwd and a recorded `repo` path never select configuration or
30
- execution authority**. Source location, deployment, work target and team
31
- membership are separate facts. Do not fill missing inputs from a config cascade,
32
- a current lock, an alias match or another instance's environment.
33
-
34
- Load **oats-portable** for the running kernel's supported captured operations.
35
- A no-launch scaffold or `launchPending` receipt is not a running agent. Where
36
- captured launch, start/restart/wake/retire or recovery is unsupported, stop and
37
- report the limitation; do not strip selectors or use a legacy command as a
38
- workaround. Preserve work, knowledge, native history, identities and outstanding
39
- cleanup receipts. Missing authority is a hold, never permission to erase state.
@@ -1,29 +0,0 @@
1
- ## Portable work mode: owned directory
2
-
3
- Your `./work` is an **instance-owned execution directory**, not a Git worktree,
4
- a checkout, or a link to the source, deployment or another instance. No Git
5
- repository or branch is created by this mode. Do not initialize a fake repository
6
- to satisfy a workflow; a containing Git repository does not grant authority over
7
- its contents.
8
-
9
- - Do task work inside `./work`. External inputs and delivery destinations require
10
- explicit task/capability authorization. Neither a source link nor a recorded
11
- `repo` or work-target path grants permission to edit that external directory.
12
- - Execution uses the explicit captured deployment/resolution and, for home-bound
13
- actions, the matching owned home/incarnation binding. Cwd does not resolve
14
- configuration or select a provider; do not rebind from a current checkout,
15
- config cascade, package lock or another instance.
16
- - Preserve home/work separation and canonical instruction aliases:
17
- `AGENTS.md` in home, `CLAUDE.md -> AGENTS.md`, and the generated skill aliases.
18
- The home's `./soul` link and retained software are read-only, not edit surfaces.
19
- - Deliver results using the task and selected capability's supported protocol.
20
- This mode imposes no knowledge layout, harvester, storage backend or publication
21
- policy. A recovery copy, if independently verified, is not publication or
22
- accepted delivery.
23
-
24
- Keep nonempty work and its custody evidence intact. This mode does not promise
25
- implemented captured launch, start/restart/wake/retire or automatic recovery.
26
- Before any supported, explicitly authorized teardown, require verified
27
- preservation of outstanding work and receipts; if that capability is unavailable,
28
- hold and report rather than deleting or moving the home/work yourself. A
29
- `launchPending` result means runtime launch remains pending.
@@ -1,120 +0,0 @@
1
- /** Exact-artifact local approval authority. Presence/catalog/legacy trust and
2
- * captured booleans never grant approval. No current selection-lock lookup. */
3
- import { join } from "node:path";
4
- import { canonicalJson, parseStrictJson } from "./portable-values.mjs";
5
- import { jsonIntegrity } from "./portable-digest.mjs";
6
- import { objectAt, versionAt } from "./portable-shape.mjs";
7
- import { validateArtifactRef, validateOrigin } from "./resolution-shape.mjs";
8
- import { verifyResolutionInputs, verifyRetainedCapability } from "./captured-resolutions.mjs";
9
- import { verifyPortableArtifact } from "./portable-artifacts.mjs";
10
- import { readLock3 } from "./portable-lock.mjs";
11
- import { executableSurfaceOf, hasExecutableSurface } from "./capability-execution.mjs";
12
- import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
13
- import { readPortableBytes } from "./portable-files.mjs";
14
- import { portableStateDirectory, withPortableStateWrite, writeGuardedPortableDocument } from "./portable-state.mjs";
15
- import { oatsError } from "./errors.mjs";
16
-
17
- const emptyLedger = () => ({ schemaVersion: 1, capabilities: Object.create(null) });
18
- const invalid = (message) => { throw oatsError("invalid-approval", message); };
19
- export function artifactApprovalKey(artifact) {
20
- validateArtifactRef(artifact);
21
- if (artifact.kind !== "capability") invalid("capability approval cannot authorize an unrelated resource bundle");
22
- return `${artifact.integrity.format}:${artifact.integrity.value}`;
23
- }
24
- export function validateApprovalLedger(ledger) {
25
- canonicalJson(ledger);
26
- objectAt(ledger, ["schemaVersion", "capabilities"], ["schemaVersion", "capabilities"]);
27
- versionAt(ledger.schemaVersion); objectAt(ledger.capabilities, null, []);
28
- for (const [id, entries] of Object.entries(ledger.capabilities)) {
29
- if (!isMaterializedCapabilityId(id)) invalid("invalid approval capability identity");
30
- objectAt(entries, null, []);
31
- for (const [key, entry] of Object.entries(entries)) {
32
- objectAt(entry, ["artifact", "approved", "provenance"], ["artifact", "approved", "provenance"]);
33
- if (artifactApprovalKey(entry.artifact) !== key || entry.artifact.capability !== id || entry.approved !== true) invalid("approval key, artifact or capability differs");
34
- if (!Array.isArray(entry.provenance) || !entry.provenance.length) invalid("approval requires explicit operator provenance");
35
- for (const origin of entry.provenance) {
36
- validateOrigin(origin);
37
- if (origin.kind !== "operator" || origin.document.kind !== "operator") invalid("approval cannot inherit source or legacy authority");
38
- }
39
- }
40
- }
41
- return ledger;
42
- }
43
- export function readApprovalLedger(scope) {
44
- const root = portableStateDirectory(scope);
45
- const bytes = root === null ? null : readPortableBytes(join(root, "approvals.json"), { allowMissing: true, invalidCode: "invalid-approval" });
46
- if (bytes === null) return { ledger: emptyLedger(), integrity: null };
47
- const ledger = parseStrictJson(bytes);
48
- validateApprovalLedger(ledger);
49
- if (!bytes.equals(Buffer.from(canonicalJson(ledger)))) invalid("approval ledger must use canonical JSON bytes");
50
- return { ledger, integrity: jsonIntegrity(ledger) };
51
- }
52
- function approved(ledger, artifact) {
53
- const key = artifactApprovalKey(artifact);
54
- return Object.hasOwn(ledger.capabilities, artifact.capability) && Object.hasOwn(ledger.capabilities[artifact.capability], key);
55
- }
56
- function manifestFor(verified, id) {
57
- return verified.manifests.get(id);
58
- }
59
-
60
- /** Diagnostic projection for this record's capabilities, not an action permit.
61
- * Dedicated helper records get their own approval check when used. */
62
- export function inspectCapturedApprovals(scope, reference) {
63
- const verified = verifyResolutionInputs(scope, reference), { ledger } = readApprovalLedger(scope);
64
- return { reference, capabilities: evaluateCapturedApprovals(verified, ledger) };
65
- }
66
-
67
- /** Shared evaluation over already verified inputs and a freshly read ledger.
68
- * This remains data; the action loader decides which surfaces the action uses. */
69
- export function evaluateCapturedApprovals(verified, ledger) {
70
- validateApprovalLedger(ledger);
71
- return Object.entries(verified.record.artifacts.capabilities).map(([id, row]) => {
72
- const manifest = manifestFor(verified, id), required = hasExecutableSurface(manifest);
73
- return { artifact: row.artifact, required, surface: executableSurfaceOf(manifest),
74
- status: !required ? "not-required" : approved(ledger, row.artifact) ? "approved" : "approval-required" };
75
- });
76
- }
77
-
78
- function grantApproval(context, artifact, manifest, origin) {
79
- const key = artifactApprovalKey(artifact), id = artifact.capability;
80
- const { ledger, integrity } = readApprovalLedger(context.deployment);
81
- if (!hasExecutableSurface(manifest)) return { artifact, status: "not-required" };
82
- if (approved(ledger, artifact)) return { artifact, status: "already-approved" };
83
- if (!Object.hasOwn(ledger.capabilities, id)) ledger.capabilities[id] = Object.create(null);
84
- ledger.capabilities[id][key] = { artifact, approved: true, provenance: [origin] };
85
- validateApprovalLedger(ledger);
86
- writeGuardedPortableDocument(context, join(context.root, "approvals.json"), ledger, { absent: integrity === null });
87
- return { artifact, status: "approved" };
88
- }
89
-
90
- /** Prospective approval must be possible BEFORE running an executable provider
91
- * codec to complete a resolution. Address an exact retained artifact set, never
92
- * today's selected capability ID or a fabricated partial resolution. */
93
- export function approveAvailableCapability(scope, setKey, id, origin, validateManifest) {
94
- validateOrigin(origin);
95
- if (origin.kind !== "operator" || origin.document.kind !== "operator") invalid("approval requires an explicit operator input");
96
- if (typeof setKey !== "string" || !/^sha256-[a-f0-9]{64}$/.test(setKey) || typeof id !== "string") invalid("invalid artifact-set approval target");
97
- if (typeof validateManifest !== "function") throw new TypeError("prospective approval requires the complete kernel manifest codec");
98
- return withPortableStateWrite(scope, (context) => {
99
- const { lock } = readLock3(context.deployment);
100
- const set = lock?.artifactSets[setKey];
101
- if (!set || !Object.hasOwn(set.capabilities, id)) invalid("capability is not in that exact retained artifact set");
102
- const artifact = set.capabilities[id].artifact;
103
- const root = verifyPortableArtifact(context.deployment, artifact).dir;
104
- const manifest = verifyRetainedCapability(root, set, id, validateManifest(root));
105
- return grantApproval(context, artifact, manifest, origin);
106
- });
107
- }
108
-
109
- /** Explicit approval writer. Called by the explicit operator approval operation,
110
- * not preparation/discovery. Approval still does not qualify provider/host readiness
111
- * or replace full manifest/launch compilation at the eventual action boundary. */
112
- export function approveCapturedCapability(scope, reference, id, origin) {
113
- validateOrigin(origin);
114
- if (origin.kind !== "operator" || origin.document.kind !== "operator") invalid("approval requires an explicit operator input");
115
- return withPortableStateWrite(scope, (context) => {
116
- const verified = verifyResolutionInputs(context.deployment, reference);
117
- if (typeof id !== "string" || !Object.hasOwn(verified.record.artifacts.capabilities, id)) invalid("approval capability is not selected in this captured record");
118
- return grantApproval(context, verified.record.artifacts.capabilities[id].artifact, manifestFor(verified, id), origin);
119
- });
120
- }
@@ -1,141 +0,0 @@
1
- /** Tree mechanics only. No acquisition, selection, trust, lock or lifecycle
2
- * policy. Callers supply the verifier used during publication. */
3
- import {
4
- chmodSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readFileSync,
5
- readdirSync, readlinkSync, renameSync, rmdirSync, rmSync, symlinkSync,
6
- } from "node:fs";
7
- import { dirname, join, relative } from "node:path";
8
- import { createHash } from "node:crypto";
9
- import { oatsError } from "./errors.mjs";
10
-
11
- /** Recursively copy a tree the way `cpSync(..., { recursive: true })` would —
12
- * except catchably.
13
- *
14
- * Node 22's recursive `cpSync` performs its recursion in native code, and on
15
- * macOS an unreadable directory inside the tree surfaces as an uncaught libc++
16
- * `filesystem_error` that TERMINATES THE PROCESS. No JS `catch` or `finally`
17
- * runs, so a transaction using it can never clean up staging or roll back the
18
- * store, the lock and the ignore file. Every package-, capability- and
19
- * user-shaped tree in the engine therefore goes through this hand-walk instead,
20
- * where an EACCES is an ordinary throwable error.
21
- *
22
- * Semantics chosen to be safe rather than maximally faithful:
23
- * - deterministic traversal (sorted entries), so two copies of one tree hash
24
- * identically;
25
- * - symlinks are recreated VERBATIM — never followed, never rewritten — because
26
- * the bytes about to be hashed must be the bytes the author wrote;
27
- * - FIFOs, sockets and device nodes are rejected fail-closed: they are not
28
- * distributable content, and copying them has no defined meaning here;
29
- * - directory modes are applied AFTER their children, so a read-only source
30
- * directory cannot block writing its own contents. */
31
- export function copyTreeSafe(src, dest) {
32
- const st = lstatSync(src);
33
- if (st.isSymbolicLink()) { symlinkSync(readlinkSync(src), dest); return; }
34
- if (st.isFile()) { copyFileSync(src, dest); chmodSync(dest, st.mode & 0o7777); return; }
35
- if (!st.isDirectory()) {
36
- throw oatsError("invalid-source", `${src} is not a regular file, directory or symlink (${st.isFIFO() ? "FIFO" : st.isSocket() ? "socket" : st.isBlockDevice() || st.isCharacterDevice() ? "device node" : "unsupported file type"}) — package and capability trees carry distributable content only`);
37
- }
38
- mkdirSync(dest, { recursive: true });
39
- for (const e of readdirSync(src, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
40
- copyTreeSafe(join(src, e.name), join(dest, e.name));
41
- }
42
- chmodSync(dest, st.mode & 0o7777);
43
- }
44
-
45
- /** Stable integrity of a MATERIALIZED capability artifact: every byte under
46
- * `.agents/capabilities/installed/<id>/`, with NO exclusions — capability source,
47
- * the materialized runtime closure (node_modules), and the generated
48
- * `.oats-installation.json` provenance file all count. This is the only digest
49
- * executable trust binds to, which is why a separate dependency digest does not
50
- * exist at capability level: the closure is inside the artifact, so tampering
51
- * with a dependency is ordinary artifact drift. */
52
- export function capabilityArtifactIntegrity(dir) {
53
- const hash = createHash("sha256");
54
- const walk = (d) => {
55
- for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
56
- const p = join(d, e.name);
57
- if (e.isDirectory()) walk(p);
58
- else if (e.isFile()) { hash.update(relative(dir, p)); hash.update("\0file\0"); hash.update(readFileSync(p)); hash.update("\0"); }
59
- else if (e.isSymbolicLink()) { hash.update(relative(dir, p)); hash.update("\0symlink\0"); hash.update(readlinkSync(p)); hash.update("\0"); }
60
- }
61
- };
62
- walk(dir);
63
- return `sha256-${hash.digest("hex")}`;
64
- }
65
-
66
- // ONLY for this preparer's unpublished staging, never source/destination trees.
67
- // copyTreeSafe preserves read-only directories after copying their children; rm's
68
- // force option does not make those directories removable. Restore owner access
69
- // top-down, using lstat at every entry so even directory symlinks stay untouched.
70
- // Files need no chmod to unlink them. Successful publication transfers the whole
71
- // staging root to the destination; the caller must not clean that root afterwards.
72
- export function removeOwnedStaging(staging) {
73
- const makeRemovable = (path) => {
74
- let st;
75
- try { st = lstatSync(path); }
76
- catch (error) { if (error.code === "ENOENT") return; throw error; }
77
- if (!st.isDirectory()) return;
78
- if ((st.mode & 0o700) !== 0o700) chmodSync(path, (st.mode & 0o7777) | 0o700);
79
- for (const name of readdirSync(path)) makeRemovable(join(path, name));
80
- };
81
- makeRemovable(staging);
82
- rmSync(staging, { recursive: true, force: true });
83
- }
84
-
85
- /** Copy into same-parent staging, verify, then publish by rename.
86
- * The staged root is a sibling of the destination: Darwin can refuse moving a
87
- * read-only directory between parents even when both parents are writable. A
88
- * sibling rename preserves the copied root's mode without rewriting its `..`.
89
- * The caller preflights the source/destination and owns existing-entry policy.
90
- * `verify` is synchronous and must throw on invalid copied or competing bytes;
91
- * this mechanical helper knows nothing about the kind of artifact or its lock.
92
- * A competing nonempty tree is reused only after verification. Rename is NOT a
93
- * general no-replace primitive: an empty destination appearing after caller
94
- * preflight can be replaced. Normal errors remove owned staging; cleanup failure
95
- * throws with its stagingPath, preserving the primary code/provenance and cause
96
- * in an AggregateError when both fail. No success receipt hides failed cleanup.
97
- * Hostile concurrent mutation, crash recovery and power-loss durability are not
98
- * promised. */
99
- export function publishArtifactTree(sourceDir, dir, verify) {
100
- const staging = mkdtempSync(join(dirname(dir), ".staging-"));
101
- const tree = staging;
102
- let failed = false, published = false, primary;
103
- try {
104
- // Preserve mechanical copy semantics for file/link roots too; retention's
105
- // verifier owns the directory/containment policy. Only remove our empty slot.
106
- if (!lstatSync(sourceDir).isDirectory()) rmdirSync(tree);
107
- copyTreeSafe(sourceDir, tree);
108
- verify(tree); // source may have changed during copy
109
- try { renameSync(tree, dir); }
110
- catch (e) {
111
- // Darwin may report EACCES rather than ENOTEMPTY for a read-only winner.
112
- // Permission denial alone proves nothing: require an observed destination,
113
- // then let the caller verify its exact bytes, shape and provenance. If it
114
- // is absent, preserve the original denial; damage is never repaired.
115
- if (e.code === "EACCES") {
116
- try { lstatSync(dir); }
117
- catch (lookup) { if (lookup.code === "ENOENT") throw e; throw lookup; }
118
- } else if (e.code !== "EEXIST" && e.code !== "ENOTEMPTY") throw e;
119
- verify(dir);
120
- return "kept";
121
- }
122
- published = true;
123
- return "retained";
124
- } catch (error) {
125
- failed = true;
126
- primary = error;
127
- throw error;
128
- } finally {
129
- try { if (!published) removeOwnedStaging(staging); }
130
- catch (cleanup) {
131
- const diagnostic = `artifact staging cleanup failed at ${staging}: ${cleanup?.message ?? cleanup}`;
132
- const error = failed
133
- ? new AggregateError([primary, cleanup], `${primary?.message ?? primary}; ${diagnostic}`, { cause: primary })
134
- : new Error(diagnostic, { cause: cleanup });
135
- error.code = failed ? primary?.code : cleanup?.code;
136
- if (failed && primary?.provenance !== undefined) error.provenance = primary.provenance;
137
- error.stagingPath = staging;
138
- throw error;
139
- }
140
- }
141
- }
@@ -1,179 +0,0 @@
1
- /** Immutable retention of already materialized capabilities. Acquisition,
2
- * selection, approval and instance dispatch remain the caller's responsibility.
3
- * See docs/design/2026-09-14-artifact-retention-contract.md. */
4
- import {
5
- linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync,
6
- readlinkSync, realpathSync, rmSync, writeFileSync,
7
- } from "node:fs";
8
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
9
- import { capabilityArtifactIntegrity, publishArtifactTree } from "./artifact-tree.mjs";
10
- import { oatsError } from "./errors.mjs";
11
- import {
12
- CAPABILITIES_DIRNAME, isMaterializedCapabilityId, validateCapabilityLockEntry,
13
- validateLockEntry, verifyCapabilityInstallation,
14
- } from "./capability-provenance.mjs";
15
-
16
- const INTEGRITY = /^sha256-[0-9a-f]{64}$/;
17
- const STORE_PARTS = [...CAPABILITIES_DIRNAME.split(sep), "artifacts"];
18
- const exists = (path) => {
19
- try { return lstatSync(path); }
20
- catch (e) { if (e.code === "ENOENT") return null; throw e; }
21
- };
22
- const outside = (root, path) => {
23
- const rel = relative(root, path);
24
- return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
25
- };
26
-
27
- /** The complete digest, not a short display prefix, identifies a revision. */
28
- export function retainedCapabilityDir(scope, capability, integrity) {
29
- if (!isMaterializedCapabilityId(capability) || typeof integrity !== "string" || !INTEGRITY.test(integrity)) {
30
- throw oatsError("invalid-artifact-reference", "retained capability requires a valid capability ID and full sha256 integrity");
31
- }
32
- return join(resolve(scope), CAPABILITIES_DIRNAME, "artifacts", capability, integrity);
33
- }
34
-
35
- function lockRows(capability, lock) {
36
- if (!lock?.capabilities || !Object.hasOwn(lock.capabilities, capability) || !lock.packages) {
37
- throw oatsError("invalid-lock", `no captured capability/package lock for ${capability}`);
38
- }
39
- for (const [id, row] of Object.entries(lock.packages)) validateLockEntry(id, row, lock.packages);
40
- const row = lock.capabilities[capability];
41
- validateCapabilityLockEntry(capability, row, lock.packages);
42
- return { row, pkg: lock.packages[row.package] };
43
- }
44
-
45
- // A retained tree must remain usable after its source disappears. In particular,
46
- // an absolute symlink pointing back into today's source is not a portable link.
47
- export function assertRetainableTree(root) {
48
- const st = exists(root);
49
- if (!st) throw oatsError("artifact-not-found", `artifact tree is absent: ${root}`);
50
- if (!st.isDirectory()) throw oatsError("invalid-artifact", `artifact root must be a directory: ${root}`);
51
- const boundary = realpathSync(root);
52
- const walk = (dir) => {
53
- for (const e of readdirSync(dir, { withFileTypes: true })) {
54
- const path = join(dir, e.name);
55
- if (e.isDirectory()) walk(path);
56
- else if (e.isSymbolicLink()) {
57
- let target;
58
- try { target = realpathSync(path); }
59
- catch { throw oatsError("artifact-not-contained", `broken artifact symlink: ${path}`); }
60
- if (isAbsolute(readlinkSync(path)) || outside(boundary, target)) {
61
- throw oatsError("artifact-not-contained", `artifact symlink must stay relative and inside its retained tree: ${path}`);
62
- }
63
- } else if (!e.isFile()) {
64
- throw oatsError("invalid-artifact", `artifact contains an unsupported file type: ${path}`);
65
- }
66
- }
67
- };
68
- walk(root);
69
- }
70
-
71
- function verifyTree(dir, capability, row, pkg) {
72
- assertRetainableTree(dir);
73
- const actual = capabilityArtifactIntegrity(dir);
74
- if (actual !== row.integrity) {
75
- throw oatsError("integrity-drift", `retained capability ${capability}: expected ${row.integrity}, got ${actual} at ${dir}`);
76
- }
77
- verifyCapabilityInstallation(dir, capability, row, pkg);
78
- }
79
-
80
- export function ensureStoreIgnore(dir) {
81
- const ignore = join(dir, ".gitignore");
82
- const verify = () => {
83
- if (!exists(ignore)?.isFile() || readFileSync(ignore, "utf8") !== "*\n") {
84
- throw oatsError("invalid-artifact-store", `managed artifact ignore file must contain exactly '*': ${ignore}`);
85
- }
86
- };
87
- if (exists(ignore)) { verify(); return; }
88
-
89
- // Exclusive creation at the public name exposes an empty/partial file. Write
90
- // and close in an owned, same-filesystem directory, then link atomically with
91
- // no replacement. EEXIST is evidence to verify, never permission to repair.
92
- // No retries, hostile-host isolation or power-loss durability are promised;
93
- // a crash can leave dot-prefixed metadata staging for explicit cleanup.
94
- const staging = mkdtempSync(join(dir, ".gitignore-"));
95
- let failed = false, primary;
96
- try {
97
- const temp = join(staging, ".gitignore");
98
- writeFileSync(temp, "*\n", { flag: "wx" });
99
- try { linkSync(temp, ignore); }
100
- catch (e) { if (e.code !== "EEXIST") throw e; }
101
- verify();
102
- } catch (error) {
103
- failed = true;
104
- primary = error;
105
- throw error;
106
- } finally {
107
- // Only our metadata staging. Never unlink the public name (even on error),
108
- // traverse another producer's staging, or change source/published modes.
109
- try { rmSync(staging, { recursive: true, force: true }); }
110
- catch (cleanup) {
111
- const diagnostic = `artifact ignore staging cleanup failed at ${staging}: ${cleanup?.message ?? cleanup}`;
112
- const error = failed
113
- ? new AggregateError([primary, cleanup], `${primary?.message ?? primary}; ${diagnostic}`, { cause: primary })
114
- : new Error(diagnostic, { cause: cleanup });
115
- error.code = failed ? primary?.code : cleanup?.code;
116
- if (failed && primary?.provenance !== undefined) error.provenance = primary.provenance;
117
- error.stagingPath = staging;
118
- throw error;
119
- }
120
- }
121
- }
122
-
123
- // The scope itself may be a user's symlink. Managed descendants must be real
124
- // directories, so a stale/broken store link cannot redirect publication.
125
- function storePath(scope, capability, create) {
126
- let dir;
127
- try { dir = realpathSync(scope); }
128
- catch (e) {
129
- if (e.code === "ENOENT") throw oatsError("artifact-not-found", `artifact scope is absent: ${scope}`);
130
- throw e;
131
- }
132
- for (const [index, part] of [...STORE_PARTS, capability].entries()) {
133
- dir = join(dir, part);
134
- if (create) {
135
- try { mkdirSync(dir); }
136
- catch (e) { if (e.code !== "EEXIST") throw e; }
137
- }
138
- const st = exists(dir);
139
- if (!st && !create) throw oatsError("artifact-not-found", `artifact store path is absent: ${dir}`);
140
- if (!st || !st.isDirectory()) throw oatsError("invalid-artifact-store", `artifact store component must be a directory: ${dir}`);
141
- if (index === STORE_PARTS.length - 1 && create) {
142
- // Owned store metadata, outside every hashed artifact. Write before any
143
- // staging payload; no Git command or change to authored directories.
144
- ensureStoreIgnore(dir);
145
- }
146
- }
147
- return dir;
148
- }
149
-
150
- /** Verify an exact retained revision using captured provenance. No current lock,
151
- * source repository, trust approval or network lookup participates. */
152
- export function verifyRetainedCapability(scope, capability, lock) {
153
- const { row, pkg } = lockRows(capability, lock);
154
- retainedCapabilityDir(scope, capability, row.integrity); // validate before path use
155
- const dir = join(storePath(scope, capability, false), row.integrity);
156
- verifyTree(dir, capability, row, pkg);
157
- return { capability, integrity: row.integrity, dir };
158
- }
159
-
160
- /** Publish once by same-filesystem rename. Never repairs or replaces a retained
161
- * revision; a damaged existing entry requires explicit operator intervention.
162
- * `lock` carries validated package/capability provenance from one resolution.
163
- * Its trust flags are not grants from this API, and are never written here. */
164
- export function retainCapabilityArtifact(scope, sourceDir, capability, lock) {
165
- const { row, pkg } = lockRows(capability, lock);
166
- retainedCapabilityDir(scope, capability, row.integrity);
167
- verifyTree(sourceDir, capability, row, pkg);
168
- const root = storePath(scope, capability, true);
169
- const dir = join(root, row.integrity);
170
- const receipt = { capability, integrity: row.integrity, dir };
171
- if (exists(dir)) {
172
- verifyTree(dir, capability, row, pkg);
173
- return { ...receipt, status: "kept" };
174
- }
175
- // Refuse recursive copies if a caller supplied an ancestor of the store.
176
- if (!outside(realpathSync(sourceDir), root)) throw oatsError("invalid-artifact", "artifact source contains its destination store");
177
- const status = publishArtifactTree(sourceDir, dir, (tree) => verifyTree(tree, capability, row, pkg));
178
- return { ...receipt, status };
179
- }
@@ -1,15 +0,0 @@
1
- /** The single existing executable-surface definition, shared with captured
2
- * approval diagnostics. This projection does not validate/compile a manifest
3
- * or permit execution; the action adapter must validate the full contract. */
4
- export function executableSurfaceOf(manifest) {
5
- return {
6
- commands: Object.keys(manifest?.commands || {}),
7
- hooks: Object.keys(manifest?.hooks || {}),
8
- environment: [...(manifest?.environment || [])],
9
- ...(manifest?.environmentNamespaces?.length ? { environmentNamespaces: [...manifest.environmentNamespaces] } : {}),
10
- };
11
- }
12
- export function hasExecutableSurface(manifest) {
13
- const s = executableSurfaceOf(manifest);
14
- return s.commands.length > 0 || s.hooks.length > 0 || s.environment.length > 0;
15
- }
@@ -1,39 +0,0 @@
1
- /** Capability-owned helper instruction policy and selected supplemental inputs.
2
- * Shape only: no source lookup, binding, readiness, side effects or fallback. */
3
- import { canonicalJson } from './portable-values.mjs';
4
- import { objectAt, invalidShape } from './portable-shape.mjs';
5
- import { portablePath } from './source-spec.mjs';
6
-
7
- export function validateHelperInjection(value) {
8
- canonicalJson(value);
9
- const file = value?.mode === 'file';
10
- objectAt(value, file ? ['version', 'mode', 'path'] : ['version', 'mode'], file ? ['version', 'mode', 'path'] : ['version', 'mode']);
11
- if (value.version !== 1 || !['inherit', 'omit', 'file'].includes(value.mode)) invalidShape('/helperInjection', 'unsupported helper instruction policy');
12
- if (file) portablePath(value.path);
13
- return value;
14
- }
15
-
16
- export function validateHookInputs(value) {
17
- canonicalJson(value);
18
- objectAt(value, ['sourceReceipt'], [], '/inputs');
19
- if (Object.hasOwn(value, 'sourceReceipt')) {
20
- objectAt(value.sourceReceipt, ['version'], ['version'], '/inputs/sourceReceipt');
21
- if (value.sourceReceipt.version !== 1) invalidShape('/inputs/sourceReceipt/version', 'unsupported source receipt input version');
22
- }
23
- return value;
24
- }
25
-
26
- /** Presence is explicit. In particular, absent/empty inputs are NOT consent;
27
- * this validator does not infer whether a provider depends on that input. */
28
- export function validateCapabilityInputDeclarations(manifest) {
29
- canonicalJson(manifest);
30
- objectAt(manifest, null, []);
31
- if (Object.hasOwn(manifest, 'helperInjection')) validateHelperInjection(manifest.helperInjection);
32
- if (Object.hasOwn(manifest, 'hooks')) {
33
- objectAt(manifest.hooks, null, [], '/hooks');
34
- for (const hook of Object.values(manifest.hooks)) {
35
- if (hook !== null && typeof hook === 'object' && Object.hasOwn(hook, 'inputs')) validateHookInputs(hook.inputs);
36
- }
37
- }
38
- return manifest;
39
- }