@awebai/oats 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/bin/oats-pi-sdk-host.mjs +17 -0
- package/bin/oats.mjs +442 -51
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
- package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
- package/capabilities/oats-okf/lib/captured-worker.mjs +101 -0
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/inspection.mjs +16 -1
- package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
- package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
- package/capabilities/oats-okf/lib/io.mjs +1 -1
- package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
- package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
- package/capabilities/oats-okf/lib/sources.mjs +123 -3
- package/capabilities/oats-okf/lib/stores.mjs +104 -25
- package/capabilities/oats-okf/lib/worker.mjs +69 -10
- package/capabilities/oats-okf/oats.json +35 -7
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
- package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
- package/docs/artifact-approvals.schema.json +7 -0
- package/docs/capability-manifest.schema.json +37 -66
- package/docs/captured-invocation-context.schema.json +7 -0
- package/docs/captured-resolution.schema.json +7 -0
- package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
- package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
- package/docs/design/2026-09-15-captured-dispatch.md +127 -0
- package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
- package/docs/design/2026-09-15-package-preparation.md +100 -0
- package/docs/design/2026-09-15-portable-data-contract.md +121 -0
- package/docs/design/2026-09-15-portable-declarations.md +189 -0
- package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
- package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
- package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
- package/docs/design/2026-09-15-source-observation.md +119 -0
- package/docs/design/2026-09-16-captured-admission.md +77 -0
- package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
- package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
- package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
- package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
- package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
- package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
- package/docs/design/2026-09-16-portable-onboarding.md +177 -0
- package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
- package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
- package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
- package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
- package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
- package/docs/design/2026-09-17-captured-native-start.md +58 -0
- package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
- package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
- package/docs/design/2026-09-17-public-captured-start.md +108 -0
- package/docs/design/2026-09-17-public-prepare-request.md +90 -0
- package/docs/design/2026-09-18-captured-pi-host.md +205 -0
- package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
- package/docs/desktop-cli-api.md +5 -2
- package/docs/execution-capsule.schema.json +108 -0
- package/docs/execution-targets.md +20 -7
- package/docs/oats-lock-v3.schema.json +7 -0
- package/docs/oats-member.schema.json +38 -0
- package/docs/oats-workspace.schema.json +68 -0
- package/docs/portable.schema.json +2512 -0
- package/docs/provider-check-input.schema.json +7 -0
- package/docs/release-notes/v0.23.2.md +49 -0
- package/docs/release-notes/v0.24.0.md +104 -0
- package/docs/schedules.md +126 -14
- package/docs/soul.schema.json +82 -0
- package/injects/oats-portable.md +17 -0
- package/injects/portable-instance-boundary.md +39 -0
- package/injects/portable-work-directory.md +29 -0
- package/lib/artifact-approvals.mjs +120 -0
- package/lib/artifact-tree.mjs +141 -0
- package/lib/capability-artifacts.mjs +179 -0
- package/lib/capability-execution.mjs +15 -0
- package/lib/capability-inputs.mjs +39 -0
- package/lib/capability-provenance.mjs +231 -0
- package/lib/captured-action-shape.mjs +21 -0
- package/lib/captured-admission-shape.mjs +20 -0
- package/lib/captured-binding-file.mjs +36 -0
- package/lib/captured-dispatch.mjs +66 -0
- package/lib/captured-instance-index.mjs +277 -0
- package/lib/captured-invocation-context.mjs +130 -0
- package/lib/captured-launch-request.mjs +46 -0
- package/lib/captured-operation-process.mjs +15 -0
- package/lib/captured-pi-custody.mjs +29 -0
- package/lib/captured-pi-host.mjs +167 -0
- package/lib/captured-pi-outcome.mjs +172 -0
- package/lib/captured-resolutions.mjs +275 -0
- package/lib/captured-scaffold.mjs +87 -0
- package/lib/captured-selector.mjs +28 -0
- package/lib/captured-session-backend.mjs +52 -0
- package/lib/captured-source-receipt-file.mjs +72 -0
- package/lib/config-data.mjs +104 -0
- package/lib/core.mjs +918 -562
- package/lib/errors.mjs +7 -0
- package/lib/helper-injection-policy.mjs +98 -0
- package/lib/herdr.mjs +18 -7
- package/lib/instruction-composition.mjs +31 -0
- package/lib/legacy-lock-codec.mjs +106 -0
- package/lib/manifest-settings.mjs +84 -0
- package/lib/package-closure.mjs +48 -0
- package/lib/package-materialization.mjs +83 -0
- package/lib/pi-sdk-host.mjs +229 -0
- package/lib/portable-artifacts.mjs +115 -0
- package/lib/portable-choices.mjs +82 -0
- package/lib/portable-composition.mjs +136 -0
- package/lib/portable-digest.mjs +105 -0
- package/lib/portable-files.mjs +26 -0
- package/lib/portable-identity.mjs +40 -0
- package/lib/portable-lock.mjs +117 -0
- package/lib/portable-migration-artifacts.mjs +135 -0
- package/lib/portable-migration-evidence.mjs +305 -0
- package/lib/portable-migration-store.mjs +199 -0
- package/lib/portable-migration.mjs +104 -0
- package/lib/portable-onboarding-acceptance.mjs +66 -0
- package/lib/portable-onboarding-request.mjs +49 -0
- package/lib/portable-onboarding.mjs +230 -0
- package/lib/portable-package-preparation.mjs +188 -0
- package/lib/portable-policy.mjs +44 -0
- package/lib/portable-shape.mjs +35 -0
- package/lib/portable-soul.mjs +38 -0
- package/lib/portable-state.mjs +80 -0
- package/lib/portable-values.mjs +181 -0
- package/lib/prepare-composition.mjs +151 -0
- package/lib/prepared-bindings.mjs +78 -0
- package/lib/prepared-resources.mjs +127 -0
- package/lib/provider-binding-broker.mjs +59 -0
- package/lib/provider-binding-wire.mjs +110 -0
- package/lib/provider-binding.mjs +22 -0
- package/lib/repository-observation.mjs +226 -0
- package/lib/resolution-shape.mjs +393 -0
- package/lib/schedule-capsule.mjs +206 -0
- package/lib/schedule.mjs +259 -38
- package/lib/servers.mjs +15 -0
- package/lib/soul-constraints.mjs +40 -0
- package/lib/source-projection.mjs +84 -0
- package/lib/source-spec.mjs +189 -0
- package/lib/workspace-definition.mjs +126 -0
- package/lib/workspace-discovery.mjs +146 -0
- package/package-catalog.json +1 -1
- package/package.json +3 -2
- package/packages/record/lib/capture-cc.mjs +14 -6
- package/packages/record/lib/formats.mjs +14 -3
- package/packages/record/lib/native-history.mjs +277 -7
- package/packages/record/lib/session-snapshot.mjs +25 -5
- package/packages/record/lib/sessions-for-home.mjs +30 -13
- package/skills/oats/SKILL.md +12 -7
- package/skills/oats-config/SKILL.md +12 -9
- package/skills/oats-packages/SKILL.md +12 -8
- package/skills/oats-portable/SKILL.md +116 -0
- package/skills/oats-portable-artifacts/SKILL.md +63 -0
- package/skills/oats-portable-setup/SKILL.md +69 -0
package/README.md
CHANGED
|
@@ -399,7 +399,10 @@ AGENTS.md, Agent Skills, and OKF.
|
|
|
399
399
|
[MIT](LICENSE) © 2026 OATS Framework
|
|
400
400
|
|
|
401
401
|
Session backends and unattended launches are described in
|
|
402
|
-
[execution targets](docs/execution-targets.md).
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
[execution targets](docs/execution-targets.md). Claude Code and Codex retain normal
|
|
403
|
+
native context and permissions **alongside the complete resolved OATS instance home**:
|
|
404
|
+
skills, capabilities, instructions, task, metadata, work placement and ordinary
|
|
405
|
+
hooks/approvals are still supplied. Add `--yolo` or select `yolo: true` in configuration
|
|
406
|
+
only for an explicit user opt-in to bypass; unattended execution does not imply it.
|
|
407
|
+
Aweb owns shared event delivery; terminal transport alone does not enable a
|
|
408
|
+
messaging broker.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Explicit kernel-owned print adapter; not an alternate interpretation of Pi CLI. */
|
|
3
|
+
import { recordCapturedPiExit, runCapturedPiSdkHost } from "../lib/captured-pi-host.mjs";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const argv = process.argv.slice(2);
|
|
7
|
+
process.exitCode = argv[0] === "--oats-pi-record-exit" ? recordCapturedPiExit(argv) : await runCapturedPiSdkHost(argv);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
// Never echo arbitrary SDK/helper/provider error objects: those can contain
|
|
10
|
+
// native auth material. Native print mode owns its ordinary safe diagnostics.
|
|
11
|
+
const known = new Set(["E_PI_HOST_ARGS", "E_PI_HOST_SELECTION", "E_PI_HOST_MODEL", "E_PI_HOST_TASK", "E_PI_HOST_SDK", "E_PI_HOST_CURRICULUM", "E_PI_HOST_HISTORY", "E_PI_HOST_CUSTODY", "E_PI_HOST_RECORD_UNAVAILABLE", "E_PI_HOST_OUTCOME"]);
|
|
12
|
+
const code = known.has(error?.code) ? error.code : "E_PI_HOST_FAILED";
|
|
13
|
+
console.error(process.argv[2] === "--oats-pi-record-exit"
|
|
14
|
+
? `${code}: captured Pi process observation refused or failed; completion evidence remains held`
|
|
15
|
+
: `${code}: captured Pi host refused or failed; use the selected harness's native setup for model/auth prerequisites`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
package/bin/oats.mjs
CHANGED
|
@@ -23,11 +23,11 @@ import { enableTmuxMouse, tmuxConfigPath, tmuxMouseEnabled } from "../lib/tmux-c
|
|
|
23
23
|
import {
|
|
24
24
|
LAYERS, WORK_MODES, LEGACY_HOME_CAPABILITIES_DIR, OATS_LOCK_FILE, OATS_VERSION, OAS_SCOPE_REMEDY, RETIRED_CAPABILITIES, detectOasScopes, retiredCapabilityReason, configChain, configCapabilityEntries, manifestOperations,
|
|
25
25
|
acquireCapability, restoreCapabilities, marketplaceCapabilities,
|
|
26
|
-
capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath,
|
|
27
|
-
readCapabilityLocks, writeCapabilityLock,
|
|
26
|
+
capabilityManifests, capabilityManifest, capabilityMissingRequires, capabilityIntegrity, capabilityTrust, capabilityExecutablePath, activateCapturedScaffold, loadCapturedDispatch, prepareCapturedComposition, resolveCapturedHelper, capturedNativeSessionAvailability, scaffoldCapturedInstance, startCapturedInstanceSession, withCapturedBindingFile, withCapturedInvocationContextFile,
|
|
27
|
+
readCapabilityLocks, writeCapabilityLock, admitCapturedAction, beginCapturedIntent, settleCapturedIntent,
|
|
28
28
|
parsePackageSource, inspectGitSourceRoot, acquirePackage, restorePackages, listInstalledPackages, readPackageLocks, readLockedConfigTemplates,
|
|
29
29
|
officialCapabilityPackage, officialPackageCatalog,
|
|
30
|
-
approveCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
|
|
30
|
+
approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
|
|
31
31
|
packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
|
|
32
32
|
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
|
|
33
33
|
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
|
|
@@ -46,9 +46,18 @@ import { parseEnvelopeText, scheduleScopeOf, listSchedules, describe as describe
|
|
|
46
46
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
47
47
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
48
48
|
|
|
49
|
+
import { capturedSelector } from "../lib/captured-selector.mjs";
|
|
50
|
+
import { inspectCapturedPiOutcome } from "../lib/captured-pi-host.mjs";
|
|
51
|
+
import { readCapturedResolution } from "../lib/captured-resolutions.mjs";
|
|
52
|
+
import { readPortablePreparationRequest } from "../lib/portable-onboarding-request.mjs";
|
|
53
|
+
import { portableScope } from "../lib/portable-state.mjs";
|
|
54
|
+
import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
|
|
55
|
+
import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
|
|
56
|
+
|
|
49
57
|
const args = process.argv.slice(2);
|
|
50
|
-
|
|
58
|
+
let cmd = args[0];
|
|
51
59
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
60
|
+
const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
52
61
|
const flag = (name) => {
|
|
53
62
|
const i = args.indexOf(`--${name}`);
|
|
54
63
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -86,6 +95,327 @@ const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
|
|
|
86
95
|
const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) } })); process.exit(1); };
|
|
87
96
|
const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result })); };
|
|
88
97
|
|
|
98
|
+
function prepareCmd() {
|
|
99
|
+
const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
|
|
100
|
+
const values = new Map(), allowed = new Set(["request", "dir", "source", "revision", "export", "alias", "workspace", "workspace-revision", "work"]);
|
|
101
|
+
for (let index = 1; index < args.length; index++) {
|
|
102
|
+
if (args[index] === "--json") continue;
|
|
103
|
+
const key = args[index].startsWith("--") ? args[index].slice(2) : "";
|
|
104
|
+
if (!allowed.has(key) || values.has(key) || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "prepare needs unique named source/context arguments; use prepare --help");
|
|
105
|
+
values.set(key, args[++index]);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
let input;
|
|
109
|
+
if (values.has("request")) {
|
|
110
|
+
// The shared leaf owns request bytes/exclusivity; this router alone owns
|
|
111
|
+
// argv and has already refused explicit captured selectors.
|
|
112
|
+
input = readPortablePreparationRequest({ file: values.get("request"),
|
|
113
|
+
inputFlags: Object.fromEntries([...values].filter(([key]) => key !== "request")) });
|
|
114
|
+
} else {
|
|
115
|
+
const deployment = values.get("dir"), alias = values.get("alias"), source = values.get("source");
|
|
116
|
+
if (!deployment || !isAbsolute(deployment) || !alias) fail("E_BAD_ARGS", "prepare needs --dir <absolute deployment> and --alias <name>");
|
|
117
|
+
if (source ? !values.get("revision") || !values.get("export") : !values.get("workspace") || values.has("revision") || values.has("export")) fail("E_BAD_ARGS", "choose a complete source/revision/export reference or a workspace-advertised alias");
|
|
118
|
+
if (values.has("workspace-revision") && !values.has("workspace")) fail("E_BAD_ARGS", "--workspace-revision requires --workspace");
|
|
119
|
+
const origin = { kind: "operator", document: { kind: "operator", id: "oats-prepare" }, pointer: "/source" };
|
|
120
|
+
input = { deployment, source: source ? { source, revision: values.get("revision"), soul: values.get("export"), alias } : alias, origin,
|
|
121
|
+
...(values.has("work") ? { mode: values.get("work") } : {}),
|
|
122
|
+
...(values.has("workspace") ? { workspace: { source: values.get("workspace"), origin: { ...origin, pointer: "/workspace" },
|
|
123
|
+
...(values.has("workspace-revision") ? { revision: values.get("workspace-revision") } : {}) } } : {}) };
|
|
124
|
+
}
|
|
125
|
+
// Pass the whole request to the one public validator/resolver. Unknown
|
|
126
|
+
// fields are refused there, never filtered or filled from ambient state.
|
|
127
|
+
const result = prepareCapturedComposition(input);
|
|
128
|
+
if (!result.resolution) fail("needs-configuration", "preparation is incomplete; no executable resolution was published", result);
|
|
129
|
+
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
130
|
+
} catch (error) { fail(error.code || "E_PREPARE_FAILED", error.message); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Run one provider operation from immutable captured authority. A home is an
|
|
134
|
+
* explicit target only: its stored binding must name this exact record. */
|
|
135
|
+
function capturedOperation(selector, load, bail) {
|
|
136
|
+
if (args[1] !== "run") bail("E_USAGE", "usage: oats operation run <layer>:<name> --deployment <abs> --resolution <id> [--home <abs>] [--arg k=v ...] [--retry-intent <saved-id>] [--json]");
|
|
137
|
+
const address = args[2], match = typeof address === "string" ? OPERATION_ADDRESS_RE.exec(address) : null;
|
|
138
|
+
if (!match) bail("E_BAD_ARGS", `operation address must be <layer>:<name> with layer one of ${LAYERS.join(", ")} (got ${JSON.stringify(address)})`);
|
|
139
|
+
const [, slot, name] = match, given = Object.create(null);
|
|
140
|
+
let home, retryExecutionId;
|
|
141
|
+
for (let index = 3; index < args.length; index++) {
|
|
142
|
+
const token = args[index];
|
|
143
|
+
if (token === "--json") continue;
|
|
144
|
+
if (token === "--home") {
|
|
145
|
+
const value = args[++index];
|
|
146
|
+
if (home !== undefined || !value || value.startsWith("--") || !isAbsolute(value)) bail("E_BAD_ARGS", "--home needs one absolute instance home");
|
|
147
|
+
home = resolve(value); continue;
|
|
148
|
+
}
|
|
149
|
+
if (token === "--retry-intent") {
|
|
150
|
+
const value = args[++index];
|
|
151
|
+
if (retryExecutionId !== undefined || !value || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) bail("E_BAD_ARGS", "--retry-intent requires one saved executionId");
|
|
152
|
+
retryExecutionId = value; continue;
|
|
153
|
+
}
|
|
154
|
+
if (token === "--arg") {
|
|
155
|
+
const value = args[++index], eq = value?.indexOf("=") ?? -1;
|
|
156
|
+
if (eq < 1) bail("E_BAD_ARGS", "--arg expects name=value");
|
|
157
|
+
const key = value.slice(0, eq);
|
|
158
|
+
if (Object.hasOwn(given, key)) bail("E_BAD_ARGS", `duplicate operation arg ${JSON.stringify(key)}`);
|
|
159
|
+
given[key] = value.slice(eq + 1); continue;
|
|
160
|
+
}
|
|
161
|
+
bail("E_BAD_ARGS", `unsupported captured operation argument ${JSON.stringify(token)}`);
|
|
162
|
+
}
|
|
163
|
+
let meta;
|
|
164
|
+
if (home) {
|
|
165
|
+
const metaFile = join(home, "instance.json");
|
|
166
|
+
if (!existsSync(metaFile)) bail("E_SESSION_UNKNOWN", `${home} is not an OATS instance home (no instance.json)`);
|
|
167
|
+
try { meta = JSON.parse(readFileSync(metaFile, "utf8")); } catch (error) { bail("E_SESSION_UNKNOWN", `${metaFile}: ${error.message}`); }
|
|
168
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta) || typeof meta.instance !== "string" || !meta.instance) bail("E_SESSION_UNKNOWN", `${metaFile}: invalid instance metadata`);
|
|
169
|
+
const binding = meta.executionBinding;
|
|
170
|
+
if (!binding) bail("migration-required", `${home} has no captured executionBinding; current configuration was not used`);
|
|
171
|
+
if (binding.schemaVersion !== 1 || typeof binding.deployment !== "string" || !isAbsolute(binding.deployment)
|
|
172
|
+
|| realOrResolved(binding.deployment) !== realOrResolved(selector.deployment)
|
|
173
|
+
|| binding.resolution?.schemaVersion !== 1 || binding.resolution.id !== selector.resolution.id) {
|
|
174
|
+
bail("E_HOME_MISMATCH", `${home} is not bound to captured resolution ${selector.resolution.id} in ${selector.deployment}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Inspect is static: validate target and arguments before the action load runs
|
|
178
|
+
// the provider's mutable readiness check.
|
|
179
|
+
const inspected = load({ kind: "inspect" }), providerId = inspected.record.bindings[slot]?.capability;
|
|
180
|
+
const provider = providerId ? inspected.manifests.get(providerId) : undefined;
|
|
181
|
+
if (!provider) bail("capability-not-selected", `no captured ${slot} provider is selected`);
|
|
182
|
+
const operation = manifestOperations(provider).find((entry) => entry.name === name);
|
|
183
|
+
if (!operation) bail("operation-not-found", "captured provider does not declare this operation");
|
|
184
|
+
if (operation.context === "home" && !meta) bail("E_OPERATION_UNAVAILABLE", `${address} runs in an instance home; pass --home <abs>`);
|
|
185
|
+
if (operation.context === "scope" && meta) bail("E_BAD_ARGS", `${address} is a scope operation and does not accept --home`);
|
|
186
|
+
const declared = new Map(operation.args.map((entry) => [entry.name, entry]));
|
|
187
|
+
for (const key of Object.keys(given)) if (!declared.has(key)) bail("E_BAD_ARGS", `${address} takes no arg ${JSON.stringify(key)} (declared: ${[...declared.keys()].join(", ") || "none"})`);
|
|
188
|
+
for (const entry of operation.args) if (entry.required && given[entry.name] === undefined) bail("E_BAD_ARGS", `${address} needs --arg ${entry.name}=<value>: ${entry.description || "required"}`);
|
|
189
|
+
const action = { kind: "operation", slot, name };
|
|
190
|
+
const argFlags = operation.args.flatMap((entry) => given[entry.name] === undefined ? [] : [entry.flag, given[entry.name]]), cwd = home || selector.deployment;
|
|
191
|
+
if (operation.kind === "action" && !home) bail("admission-required", "scope mutation has no qualified incarnation/admission path; use an instance-scoped operation");
|
|
192
|
+
if (retryExecutionId !== undefined && operation.kind !== "action") bail("E_BAD_ARGS", "read-only operations do not retry mutation intents");
|
|
193
|
+
const admission = operation.kind === "action" ? admitCapturedAction({ deployment: selector.deployment, resolution: selector.resolution, home, action, input: { arguments: argFlags },
|
|
194
|
+
...(retryExecutionId !== undefined ? { retryExecutionId } : {}) }) : null;
|
|
195
|
+
let settlement;
|
|
196
|
+
const admittedBail = (code, message, details) => bail(code, message, { ...details, ...(admission ? { intent: admission.intent } : {}),
|
|
197
|
+
...(settlement ? { settlement, unconfirmed: settlement.state === "unconfirmed" } : {}) });
|
|
198
|
+
if (admission?.replayed) {
|
|
199
|
+
settlement = { state: "completed", receipt: admission.receipt };
|
|
200
|
+
if (!admission.replayable) admittedBail("needs-configuration", "completed operation cannot replay its retained outcome");
|
|
201
|
+
finishOperation({ r: { status: 0, stdout: JSON.stringify(admission.receipt) }, bail: admittedBail, address, provider, op: operation, argFlags, cwd, home, meta, intent: admission.intent }); return;
|
|
202
|
+
}
|
|
203
|
+
let loaded;
|
|
204
|
+
try { loaded = load(action, { invocationTarget: meta ? { home, work: join(home, "work"), name: meta.instance, agent: meta.agent } : null,
|
|
205
|
+
...(admission ? { intent: admission.intent, priorReceipt: admission.receipt } : {}) }); }
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (admission) {
|
|
208
|
+
settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state: "blocked", receipt: admission.receipt });
|
|
209
|
+
settlement = { state: "blocked", receipt: admission.receipt };
|
|
210
|
+
}
|
|
211
|
+
admittedBail(error.code || "provider-unavailable", error.message);
|
|
212
|
+
}
|
|
213
|
+
const { capability, executable } = loaded;
|
|
214
|
+
const env = { ...process.env };
|
|
215
|
+
for (const key of Object.keys(env)) if (key.startsWith("OATS_") || key.startsWith("PI_AGENT_") || key === "PI_AGENTS_ROOT") delete env[key];
|
|
216
|
+
Object.assign(env, {
|
|
217
|
+
OATS_DEPLOYMENT: selector.deployment, OATS_RESOLUTION: selector.resolution.id,
|
|
218
|
+
OATS_CAPABILITY: capability.id, OATS_CAPABILITY_ROOT: capability.manifest._dir,
|
|
219
|
+
OATS_SETTINGS: JSON.stringify(capability.settings), OATS_CLI_BIN: CLI_BIN,
|
|
220
|
+
OATS_OPERATION: address, OATS_CONTEXT: selector.deployment, OATS_LEVEL: selector.deployment,
|
|
221
|
+
OATS_WORKSPACE: selector.deployment,
|
|
222
|
+
});
|
|
223
|
+
if (meta) Object.assign(env, { OATS_INSTANCE: meta.instance, OATS_INSTANCE_HOME: home, OATS_HOME: home,
|
|
224
|
+
PI_AGENT_INSTANCE: meta.instance, PI_AGENT_HOME: home, ...(meta.agent ? { OATS_AGENT: meta.agent } : {}) });
|
|
225
|
+
const invocation = loaded.invocation;
|
|
226
|
+
let child, cleanupError, started = false;
|
|
227
|
+
try {
|
|
228
|
+
child = withCapturedInvocationContextFile(invocation, contextEnv => withCapturedBindingFile(loaded, bindingEnv => {
|
|
229
|
+
if (admission) { beginCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action }); started = true; }
|
|
230
|
+
return runCapturedOperationProcess({ file: executable.file, args: [...executable.args, ...argFlags, "--json"], cwd, env: { ...env, ...contextEnv, ...bindingEnv } });
|
|
231
|
+
}));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (!error?.invocationCompleted) {
|
|
234
|
+
if (admission) {
|
|
235
|
+
settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state: started ? "unconfirmed" : "blocked", receipt: admission.receipt });
|
|
236
|
+
settlement = { state: started ? "unconfirmed" : "blocked", receipt: admission.receipt };
|
|
237
|
+
}
|
|
238
|
+
admittedBail(error.code || "E_OPERATION_RESULT", error.message, { unconfirmed: started });
|
|
239
|
+
}
|
|
240
|
+
child = error.invocationResult; cleanupError = error;
|
|
241
|
+
}
|
|
242
|
+
if (admission) {
|
|
243
|
+
let envelope; try { envelope = JSON.parse(String(child.stdout || "").trim()); } catch { /* unconfirmed below */ }
|
|
244
|
+
const completed = !cleanupError && !child.error && child.status === 0 && envelope?.schemaVersion === 1 && envelope.ok === true;
|
|
245
|
+
const state = completed ? "completed" : "unconfirmed", receipt = envelope ?? parseEnvelopeText(String(child.stdout || "")) ?? admission.receipt;
|
|
246
|
+
try {
|
|
247
|
+
settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state, receipt, replayable: completed });
|
|
248
|
+
settlement = { state, receipt };
|
|
249
|
+
}
|
|
250
|
+
catch (error) { admittedBail("E_OPERATION_RESULT", "operation ran but outcome custody could not be confirmed", { unconfirmed: true, envelope, custody: { code: error.code, message: error.message } }); }
|
|
251
|
+
}
|
|
252
|
+
finishOperation({ r: child, bail: admittedBail, address, provider: capability.manifest, op: operation, argFlags, cwd, home, meta, cleanupError, settlement, ...(admission ? { intent: admission.intent } : {}) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Fresh explicit captured scaffold + spawn hooks. Placement is supplied by
|
|
256
|
+
* the operator; launch and non-directory work remain unsupported. */
|
|
257
|
+
function capturedSpawn(selector, load, bail) {
|
|
258
|
+
const subject = args[1]; let home; let noLaunch = false;
|
|
259
|
+
if (!subject || subject.startsWith("-")) bail("E_BAD_ARGS", "captured spawn needs the retained subject name");
|
|
260
|
+
for (let index = 2; index < args.length; index++) {
|
|
261
|
+
const token = args[index];
|
|
262
|
+
if (token === "--json") continue;
|
|
263
|
+
if (token === "--no-launch") { if (noLaunch) bail("E_BAD_ARGS", "duplicate --no-launch"); noLaunch = true; continue; }
|
|
264
|
+
if (token === "--home") {
|
|
265
|
+
const value = args[++index];
|
|
266
|
+
if (home !== undefined || !value || value.startsWith("--") || !isAbsolute(value)) bail("E_BAD_ARGS", "--home needs one absolute new instance home");
|
|
267
|
+
home = resolve(value); continue;
|
|
268
|
+
}
|
|
269
|
+
bail("E_BAD_ARGS", `unsupported captured spawn argument ${JSON.stringify(token)}`);
|
|
270
|
+
}
|
|
271
|
+
if (!home || !noLaunch) bail("E_BAD_ARGS", "captured spawn currently requires --home <absolute new home> and --no-launch");
|
|
272
|
+
const inspected = load({ kind: "inspect" }), expected = inspected.record.subject.kind === "persistent" ? inspected.record.subject.soul.alias : inspected.record.subject.name;
|
|
273
|
+
if (subject !== expected) bail("E_HOME_MISMATCH", `captured resolution subject is ${expected}, not ${subject}`);
|
|
274
|
+
const scaffold = scaffoldCapturedInstance({ deployment: selector.deployment, resolution: selector.resolution, home, instance: basename(home) });
|
|
275
|
+
let activated;
|
|
276
|
+
try {
|
|
277
|
+
activated = activateCapturedScaffold({ deployment: selector.deployment, resolution: selector.resolution, home,
|
|
278
|
+
extraEnv: process.env.OATS_HOME_DIR ? { OATS_HOME_DIR: process.env.OATS_HOME_DIR } : {} });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error?.home) bail(error.code || "E_SPAWN_FAILED", error.message, { home: error.home, cleanupRequired: true, failures: error.provenance || [],
|
|
281
|
+
...(error.capturedCustody ? { unconfirmed: true, custody: error.capturedCustody } : {}) });
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
const result = { ...scaffold, hooksPending: activated.hooksPending, cleanupRequired: activated.cleanupRequired, launchPending: true,
|
|
285
|
+
hookIntents: activated.hooks.intents, hookOrder: activated.hooks.order, warnings: activated.hooks.warnings };
|
|
286
|
+
if (JSON_MODE) jsonOk(result); else console.log(`Scaffolded ${result.instance} at ${result.home}; ${result.hooksPending ? "captured hook custody requires retry/reconciliation" : "captured hooks complete"}, launch pending`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Native continuation of an already owned captured home. A helper selector is
|
|
290
|
+
* an exact edge from the SOURCE record, not a name/current-config resolver. */
|
|
291
|
+
function capturedSession(selector, bail) {
|
|
292
|
+
const inspecting = args[1] === "inspect";
|
|
293
|
+
if (!["start", "restart", "inspect"].includes(args[1])) bail("unsupported-action", "captured session supports start/restart or explicit Pi outcome inspect; no current-context fallback was used");
|
|
294
|
+
const values = new Map(), allowed = new Set(inspecting ? ["home", "helper", "native-record"] : ["home", "helper", "request", "retry-intent"]);
|
|
295
|
+
for (let index = 2; index < args.length; index++) {
|
|
296
|
+
if (args[index] === "--json") continue;
|
|
297
|
+
const key = args[index].startsWith("--") ? args[index].slice(2) : "", value = args[index + 1];
|
|
298
|
+
if (!allowed.has(key) || values.has(key) || !value || value.startsWith("--")) bail("E_BAD_ARGS", "captured session needs unique named home/helper/request/retry arguments");
|
|
299
|
+
values.set(key, value); index++;
|
|
300
|
+
}
|
|
301
|
+
const home = values.get("home"), retry = values.get("retry-intent");
|
|
302
|
+
if (!home || !isAbsolute(home) || resolve(home) !== home || home.includes("\0")) bail("E_BAD_ARGS", "captured session needs a normalized absolute --home");
|
|
303
|
+
if (retry !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(retry)) bail("E_BAD_ARGS", "--retry-intent requires one saved executionId");
|
|
304
|
+
if (inspecting && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(values.get("native-record") ?? "")) bail("E_BAD_ARGS", "captured Pi outcome inspect requires one explicit --native-record UUID");
|
|
305
|
+
const sourceExecutionBinding = { schemaVersion: 1, deployment: portableScope(selector.deployment), resolution: selector.resolution };
|
|
306
|
+
// Pure retained-subject check, before request files, provider or native calls.
|
|
307
|
+
// Dedicated helper IDs remain valid for scaffolding, not edge-less dispatch.
|
|
308
|
+
if (!values.has("helper") && readCapturedResolution(sourceExecutionBinding.deployment, sourceExecutionBinding.resolution).subject.kind === "helper") {
|
|
309
|
+
bail("helper-not-selected", "captured helper session needs SOURCE selectors plus --helper EXACT_SOURCE_HELPER_KEY");
|
|
310
|
+
}
|
|
311
|
+
// Reuse the same bounded strict object-file transport. Preparation and native
|
|
312
|
+
// request schemas remain separate; reject unknown fields before projection.
|
|
313
|
+
let request = {};
|
|
314
|
+
if (values.has("request")) {
|
|
315
|
+
const input = readPortablePreparationRequest({ file: values.get("request") });
|
|
316
|
+
if (input.schemaVersion !== 1 || Object.keys(input).some(key => !["schemaVersion", "backend", "task", "stopGraceMs"].includes(key))) bail("E_BAD_ARGS", "native request must be version1 with only backend/task/stopGraceMs");
|
|
317
|
+
if (Object.hasOwn(input, "backend") && (!input.backend || typeof input.backend !== "object" || Array.isArray(input.backend))) bail("E_BAD_ARGS", "a supplied native backend must be an explicit object, not omission");
|
|
318
|
+
const { schemaVersion, ...fields } = input; request = fields;
|
|
319
|
+
}
|
|
320
|
+
const helperSelection = values.has("helper") ? resolveCapturedHelper({ executionBinding: sourceExecutionBinding, helper: values.get("helper") }) : null;
|
|
321
|
+
const executionBinding = helperSelection?.executionBinding ?? sourceExecutionBinding;
|
|
322
|
+
try {
|
|
323
|
+
if (inspecting) {
|
|
324
|
+
const outcome = inspectCapturedPiOutcome(home, { ...executionBinding, nativeRecordId: values.get("native-record") });
|
|
325
|
+
const result = { outcome, executionBinding, ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}) };
|
|
326
|
+
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const native = startCapturedInstanceSession(home, { ...request, deployment: executionBinding.deployment, resolution: executionBinding.resolution,
|
|
330
|
+
restart: args[1] === "restart", ...(retry !== undefined ? { retryExecutionId: retry } : {}) });
|
|
331
|
+
const result = { ...native, executionBinding, ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}) };
|
|
332
|
+
if (JSON_MODE) jsonOk(result); else console.log(`${result.replayed ? "Replayed captured dispatch receipt for" : "Dispatched captured native session for"} ${home}`);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
bail(error.code || "E_SESSION_START_FAILED", error.message, { home: error.home ?? home, executionBinding,
|
|
335
|
+
...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}),
|
|
336
|
+
...(error.capturedCustody ? { custody: error.capturedCustody } : {}),
|
|
337
|
+
...(error.nativeCustody ? { unconfirmed: true, nativeCustody: error.nativeCustody } : {}) });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Exact-selector dispatch enters before any current-context resolver. Its
|
|
342
|
+
* child receives the same selector, never an invoking agent's ambient identity. */
|
|
343
|
+
function capturedCommand(selector) {
|
|
344
|
+
const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
|
|
345
|
+
try {
|
|
346
|
+
const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
|
|
347
|
+
const permitsHome = cmd === "operation" || cmd === "spawn" || cmd === "session";
|
|
348
|
+
const forbiddenContext = permitsHome ? ["--dir", "--server", "--soul", "--agents-root"] : ["--dir", "--home", "--server", "--soul", "--agents-root"];
|
|
349
|
+
if (head.some((arg) => forbiddenContext.includes(arg.split("=")[0]))) {
|
|
350
|
+
fail("E_BAD_ARGS", "captured selectors cannot be mixed with current-context selectors");
|
|
351
|
+
}
|
|
352
|
+
if (selector.artifactSet !== undefined) {
|
|
353
|
+
if (cmd !== "trust" || !args[1] || args[1].startsWith("-") || args.slice(2).some((arg) => arg !== "--json")) fail("E_BAD_ARGS", "artifact-set selectors support only explicit trust of one capability");
|
|
354
|
+
const result = approveAvailableCapability(selector.deployment, selector.artifactSet, args[1], {
|
|
355
|
+
kind: "operator", document: { kind: "operator", id: "oats-trust-artifact-set" }, pointer: "/capability",
|
|
356
|
+
});
|
|
357
|
+
if (JSON_MODE) jsonOk(result); else console.log(`${args[1]}: ${result.status}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const target = { deployment: selector.deployment, resolution: selector.resolution };
|
|
361
|
+
const load = (action, extra = {}) => loadCapturedDispatch({ ...target, action, ...extra });
|
|
362
|
+
if (cmd === "inspect") {
|
|
363
|
+
let helperKey;
|
|
364
|
+
for (let index = 1; index < args.length; index++) {
|
|
365
|
+
if (["--json", "--composition"].includes(args[index])) continue;
|
|
366
|
+
if (args[index] !== "--helper" || helperKey !== undefined || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "captured inspect accepts --json, --composition and one --helper <exact-map-key>");
|
|
367
|
+
helperKey = args[++index];
|
|
368
|
+
}
|
|
369
|
+
const helperSelection = helperKey === undefined ? null : resolveCapturedHelper({ executionBinding: { schemaVersion: 1, ...target }, helper: helperKey });
|
|
370
|
+
const selected = helperSelection?.executionBinding ?? target;
|
|
371
|
+
const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
|
|
372
|
+
const result = { resolution: loaded.resolution, capture: loaded.record.capture, nativeSession: capturedNativeSessionAvailability(),
|
|
373
|
+
...(helperSelection ? { helperSelection } : {}),
|
|
374
|
+
capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
|
|
375
|
+
approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
|
|
376
|
+
helpers: Object.entries(loaded.record.helpers).map(([key, resolution]) => ({ key, resolution })),
|
|
377
|
+
hasComposition: !!loaded.record.dispatch.composition,
|
|
378
|
+
...(loaded.composition ? { composition: loaded.composition } : {}) };
|
|
379
|
+
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (cmd === "trust") {
|
|
383
|
+
if (!args[1] || args[1].startsWith("-") || args.slice(2).some((arg) => arg !== "--json")) fail("E_BAD_ARGS", "captured trust needs one capability ID");
|
|
384
|
+
load({ kind: "inspect" }); // complete manifest validation before approval
|
|
385
|
+
const result = approveCapturedCapability(selector.deployment, selector.resolution, args[1], {
|
|
386
|
+
kind: "operator", document: { kind: "operator", id: "oats-trust" }, pointer: "/capability",
|
|
387
|
+
});
|
|
388
|
+
if (JSON_MODE) jsonOk(result); else console.log(`${args[1]}: ${result.status}`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (cmd === "operation") { capturedOperation(selector, load, fail); return; }
|
|
392
|
+
if (cmd === "spawn") { capturedSpawn(selector, load, fail); return; }
|
|
393
|
+
if (cmd === "session") { capturedSession(selector, fail); return; }
|
|
394
|
+
if (!cmd || cmd.startsWith("-") || KERNEL_COMMANDS.has(cmd)) fail("unsupported-action", "this kernel command has not yet adopted captured selectors; no current-context fallback was used");
|
|
395
|
+
if (!args[1] || args[1] === "--json" || head.some((arg) => HELP_WORDS.has(arg))) {
|
|
396
|
+
const loaded = load({ kind: "inspect" });
|
|
397
|
+
const matches = [...loaded.manifests.values()].filter((manifest) => manifest.command === cmd);
|
|
398
|
+
if (matches.length !== 1) fail("capability-not-selected", "captured command namespace is absent or ambiguous");
|
|
399
|
+
const result = { capability: matches[0].capability, commands: Object.keys(matches[0].commands || {}), help: "manifest only; no executable ran" };
|
|
400
|
+
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const loaded = load({ kind: "command", namespace: cmd, name: args[1] });
|
|
404
|
+
const env = { ...process.env };
|
|
405
|
+
for (const key of Object.keys(env)) if (key.startsWith("OATS_") || key.startsWith("PI_AGENT_") || key === "PI_AGENTS_ROOT") delete env[key];
|
|
406
|
+
Object.assign(env, { OATS_DEPLOYMENT: selector.deployment, OATS_RESOLUTION: selector.resolution.id,
|
|
407
|
+
OATS_CAPABILITY: loaded.capability.id, OATS_CAPABILITY_ROOT: loaded.capability.manifest._dir,
|
|
408
|
+
OATS_SETTINGS: JSON.stringify(loaded.capability.settings),
|
|
409
|
+
OATS_CLI_BIN: CLI_BIN, OATS_CONTEXT: selector.deployment, OATS_LEVEL: selector.deployment });
|
|
410
|
+
const forwarded = args.slice(2); if (forwarded[0] === "--") forwarded.shift();
|
|
411
|
+
const child = withCapturedInvocationContextFile(loaded.invocation, contextEnv => withCapturedBindingFile(loaded, bindingEnv => spawnSync(process.execPath, [loaded.executable.file, ...loaded.executable.args, ...forwarded], {
|
|
412
|
+
cwd: selector.deployment, env: { ...env, ...contextEnv, ...bindingEnv }, stdio: "inherit",
|
|
413
|
+
})));
|
|
414
|
+
if (child.error) fail("E_CAPABILITY_BROKEN", child.error.message);
|
|
415
|
+
process.exit(child.status ?? 1);
|
|
416
|
+
} catch (error) { fail(error.code || "E_CAPABILITY_BROKEN", error.message); }
|
|
417
|
+
}
|
|
418
|
+
|
|
89
419
|
/** Level of a directory: laptop (home), repo (.git), else workspace. */
|
|
90
420
|
function levelOf(dir) {
|
|
91
421
|
const d = resolve(dir);
|
|
@@ -637,7 +967,55 @@ const OPERATION_ADDRESS_RE = /^(knowledge|messaging|tasks):([a-z][a-z0-9-]*)$/;
|
|
|
637
967
|
const reportsRetainedEffectsText = (message) => /INCOMPLETE|quarantin|retain|could not (?:be )?(?:verif|confirm)/i.test(String(message || ""));
|
|
638
968
|
// Comfortably below the scheduler's 5-minute command bound and any GUI
|
|
639
969
|
// proxy, so the receipt always reaches the caller before a wrapper gives up.
|
|
640
|
-
const OPERATION_TIMEOUT_MS =
|
|
970
|
+
const OPERATION_TIMEOUT_MS = CAPTURED_OPERATION_TIMEOUT_MS;
|
|
971
|
+
function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta, cleanupError, intent, settlement }) {
|
|
972
|
+
const stderr = String(r.stderr || "").trim();
|
|
973
|
+
const timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && ["SIGTERM", "SIGKILL"].includes(r.signal) && (!settlement || !r.error));
|
|
974
|
+
const base = { operation: address, capability: provider.capability, version: provider.version || null, argv: [provider.command, op.command, ...argFlags], cwd, target: home ? { home, instance: meta.instance } : null };
|
|
975
|
+
// Unconfirmed outcomes (a timeout, no valid receipt, a receipt contradicted
|
|
976
|
+
// by the exit status) carry what WAS observed in error.details, so a
|
|
977
|
+
// scheduler can keep the slot as unknown and reconcile by any name the
|
|
978
|
+
// provider managed to answer; they are never confirmed failures.
|
|
979
|
+
const observed = (envelope) => ({ exit: r.status, signal: r.signal || null, unconfirmed: true, ...(envelope && typeof envelope === "object" ? { envelope } : {}),
|
|
980
|
+
...(stderr ? { stderr: stderr.slice(0, 2000) } : {}), ...(cleanupError ? { cleanup: { code: cleanupError.code || "E_OPERATION_CLEANUP", message: String(cleanupError.message || cleanupError).slice(0, 1000) } } : {}) });
|
|
981
|
+
if (r.error && !timedOut) bail("E_CAPABILITY_BROKEN", `${address}: ${r.error.message || r.error}`, settlement ? observed(parseEnvelopeText(String(r.stdout || ""))) : undefined);
|
|
982
|
+
if (timedOut) bail("E_OPERATION_TIMEOUT", `${address} (${provider.capability} ${op.command}) did not finish within ${OPERATION_TIMEOUT_MS / 1000} s; its effects are unconfirmed`, observed(parseEnvelopeText(String(r.stdout || ""))));
|
|
983
|
+
// Exactly one JSON-v1 envelope on stdout, nothing else, and an exit status
|
|
984
|
+
// that agrees with it: contaminated output or a success envelope from a
|
|
985
|
+
// process that then failed is not a receipt.
|
|
986
|
+
let envelope;
|
|
987
|
+
try { envelope = JSON.parse(String(r.stdout || "").trim()); } catch { envelope = undefined; }
|
|
988
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope) || envelope.schemaVersion !== 1 || typeof envelope.ok !== "boolean") bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) did not answer exactly one JSON-v1 envelope on stdout (exit ${r.status}); its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(parseEnvelopeText(String(r.stdout || ""))));
|
|
989
|
+
if (cleanupError) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered, but private invocation cleanup could not be confirmed; its effects are unconfirmed`, observed(envelope));
|
|
990
|
+
// A provider's own failure is relayed with its code; its WHOLE envelope
|
|
991
|
+
// (a partial receipt such as result.instance of something it launched
|
|
992
|
+
// before failing, and any details it gave) travels in error.details so a
|
|
993
|
+
// scheduler can keep an unconfirmed outcome and reconcile that target.
|
|
994
|
+
if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
|
|
995
|
+
if (r.status !== 0) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered ok but exited ${r.status}; the receipt is not trusted and its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(envelope));
|
|
996
|
+
const result = envelope.result && typeof envelope.result === "object" ? envelope.result : {};
|
|
997
|
+
if (op.kind === "view") {
|
|
998
|
+
const docs = result.documents;
|
|
999
|
+
const bad = !Array.isArray(docs) || docs.some((d) => !d || typeof d !== "object" || typeof d.label !== "string" || !d.label
|
|
1000
|
+
|| (d.kind !== undefined && !["markdown", "text"].includes(d.kind))
|
|
1001
|
+
|| (d.path !== undefined && d.path !== null && (typeof d.path !== "string" || !isAbsolute(d.path)))
|
|
1002
|
+
|| (d.text !== undefined && d.text !== null && typeof d.text !== "string"));
|
|
1003
|
+
if (bad) bail("E_OPERATION_RESULT", `${address} is a view operation but ${provider.capability} ${op.command} did not answer { documents: [{label, kind?, path?, text?}] }`);
|
|
1004
|
+
}
|
|
1005
|
+
// A launch receipt in the delegated result (a harvester the provider
|
|
1006
|
+
// spawned) is surfaced as top-level instance/home so the scheduler tracks
|
|
1007
|
+
// it exactly as it tracks a command job's launch; the source home itself
|
|
1008
|
+
// is `target`, never a launch.
|
|
1009
|
+
const receipt = {};
|
|
1010
|
+
if (typeof result.instance === "string" && result.instance !== meta?.instance) receipt.instance = result.instance;
|
|
1011
|
+
if (typeof result.home === "string" && result.home !== home) receipt.home = result.home;
|
|
1012
|
+
const out = { ...base, ...receipt, result, ...(intent ? { intent } : {}), ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}) };
|
|
1013
|
+
if (JSON_MODE) { jsonOk(out); return; }
|
|
1014
|
+
console.log(`${address} via ${provider.capability}@${provider.version || "?"} (${provider.command} ${op.command}) in ${shortPath(cwd)}: ok`);
|
|
1015
|
+
if (op.kind === "view") for (const d of result.documents) console.log(` - ${d.label}${d.path ? ` (${shortPath(d.path)})` : ""}${d.text ? `: ${String(d.text).split("\n")[0].slice(0, 100)}` : ""}`);
|
|
1016
|
+
else console.log(JSON.stringify(result, null, 2));
|
|
1017
|
+
if (stderr) console.error(stderr);
|
|
1018
|
+
}
|
|
641
1019
|
function operationCmd() {
|
|
642
1020
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
643
1021
|
dropAmbientRoot();
|
|
@@ -754,50 +1132,7 @@ function operationCmd() {
|
|
|
754
1132
|
});
|
|
755
1133
|
if (op.context === "home") Object.assign(env, { OATS_INSTANCE: meta.instance, OATS_INSTANCE_HOME: home, OATS_HOME: home, PI_AGENT_INSTANCE: meta.instance, PI_AGENT_HOME: home });
|
|
756
1134
|
const r = spawnSync("node", [abs, ...rest, ...argFlags, "--json"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024, timeout: OPERATION_TIMEOUT_MS, killSignal: "SIGTERM" });
|
|
757
|
-
|
|
758
|
-
const timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && r.signal === "SIGTERM");
|
|
759
|
-
if (r.error && !timedOut) bail("E_CAPABILITY_BROKEN", `${address}: ${r.error.message || r.error}`);
|
|
760
|
-
const base = { operation: address, capability: provider.capability, version: provider.version || null, argv: [provider.command, op.command, ...argFlags], cwd, target: home ? { home, instance: meta.instance } : null };
|
|
761
|
-
// Unconfirmed outcomes (a timeout, no valid receipt, a receipt contradicted
|
|
762
|
-
// by the exit status) carry what WAS observed in error.details, so a
|
|
763
|
-
// scheduler can keep the slot as unknown and reconcile by any name the
|
|
764
|
-
// provider managed to answer; they are never confirmed failures.
|
|
765
|
-
const observed = (envelope) => ({ exit: r.status, signal: r.signal || null, unconfirmed: true, ...(envelope && typeof envelope === "object" ? { envelope } : {}), ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}) });
|
|
766
|
-
if (timedOut) bail("E_OPERATION_TIMEOUT", `${address} (${provider.capability} ${op.command}) did not finish within ${OPERATION_TIMEOUT_MS / 1000} s; its effects are unconfirmed`, observed(parseEnvelopeText(String(r.stdout || ""))));
|
|
767
|
-
// Exactly one JSON-v1 envelope on stdout, nothing else, and an exit status
|
|
768
|
-
// that agrees with it: contaminated output or a success envelope from a
|
|
769
|
-
// process that then failed is not a receipt.
|
|
770
|
-
let envelope;
|
|
771
|
-
try { envelope = JSON.parse(String(r.stdout || "").trim()); } catch { envelope = undefined; }
|
|
772
|
-
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope) || envelope.schemaVersion !== 1 || typeof envelope.ok !== "boolean") bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) did not answer exactly one JSON-v1 envelope on stdout (exit ${r.status}); its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(parseEnvelopeText(String(r.stdout || ""))));
|
|
773
|
-
// A provider's own failure is relayed with its code; its WHOLE envelope
|
|
774
|
-
// (a partial receipt such as result.instance of something it launched
|
|
775
|
-
// before failing, and any details it gave) travels in error.details so a
|
|
776
|
-
// scheduler can keep an unconfirmed outcome and reconcile that target.
|
|
777
|
-
if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
|
|
778
|
-
if (r.status !== 0) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered ok but exited ${r.status}; the receipt is not trusted and its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(envelope));
|
|
779
|
-
const result = envelope.result && typeof envelope.result === "object" ? envelope.result : {};
|
|
780
|
-
if (op.kind === "view") {
|
|
781
|
-
const docs = result.documents;
|
|
782
|
-
const bad = !Array.isArray(docs) || docs.some((d) => !d || typeof d !== "object" || typeof d.label !== "string" || !d.label
|
|
783
|
-
|| (d.kind !== undefined && !["markdown", "text"].includes(d.kind))
|
|
784
|
-
|| (d.path !== undefined && d.path !== null && (typeof d.path !== "string" || !isAbsolute(d.path)))
|
|
785
|
-
|| (d.text !== undefined && d.text !== null && typeof d.text !== "string"));
|
|
786
|
-
if (bad) bail("E_OPERATION_RESULT", `${address} is a view operation but ${provider.capability} ${op.command} did not answer { documents: [{label, kind?, path?, text?}] }`);
|
|
787
|
-
}
|
|
788
|
-
// A launch receipt in the delegated result (a harvester the provider
|
|
789
|
-
// spawned) is surfaced as top-level instance/home so the scheduler tracks
|
|
790
|
-
// it exactly as it tracks a command job's launch; the source home itself
|
|
791
|
-
// is `target`, never a launch.
|
|
792
|
-
const receipt = {};
|
|
793
|
-
if (typeof result.instance === "string" && result.instance !== meta?.instance) receipt.instance = result.instance;
|
|
794
|
-
if (typeof result.home === "string" && result.home !== home) receipt.home = result.home;
|
|
795
|
-
const out = { ...base, ...receipt, result, ...(stderr ? { stderr: stderr.slice(0, 2000) } : {}) };
|
|
796
|
-
if (JSON_MODE) { jsonOk(out); return; }
|
|
797
|
-
console.log(`${address} via ${provider.capability}@${provider.version || "?"} (${provider.command} ${op.command}) in ${shortPath(cwd)}: ok`);
|
|
798
|
-
if (op.kind === "view") for (const d of result.documents) console.log(` - ${d.label}${d.path ? ` (${shortPath(d.path)})` : ""}${d.text ? `: ${String(d.text).split("\n")[0].slice(0, 100)}` : ""}`);
|
|
799
|
-
else console.log(JSON.stringify(result, null, 2));
|
|
800
|
-
if (stderr) console.error(stderr);
|
|
1135
|
+
finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta });
|
|
801
1136
|
}
|
|
802
1137
|
|
|
803
1138
|
// ---------- soul set: runtime defaults and instructions of an editable soul ----------
|
|
@@ -3822,6 +4157,7 @@ function scheduleCmd() {
|
|
|
3822
4157
|
|
|
3823
4158
|
async function sessionCmd() {
|
|
3824
4159
|
try {
|
|
4160
|
+
if (flag("native-record") !== undefined) throw Object.assign(new Error("--native-record outcome inspection requires exact captured deployment/resolution selectors"), { code: "E_BAD_ARGS" });
|
|
3825
4161
|
const home = flag("home");
|
|
3826
4162
|
let result;
|
|
3827
4163
|
if (args[1] === "attach") {
|
|
@@ -4158,7 +4494,7 @@ function versionCmd() {
|
|
|
4158
4494
|
// on it (an older CLI without the surface must fail closed with a
|
|
4159
4495
|
// reason, not an argument error). `features`: kernel abilities a peer
|
|
4160
4496
|
// must see before relying on them (retire-home: retire --home).
|
|
4161
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations"], scheduleApi: SCHEDULE_API, operationsApi: 1 }));
|
|
4497
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations"], scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
|
|
4162
4498
|
return;
|
|
4163
4499
|
}
|
|
4164
4500
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -4523,11 +4859,38 @@ async function serverRouteCmd() {
|
|
|
4523
4859
|
// blame` pointing at the commit that last changed each command.
|
|
4524
4860
|
const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
|
|
4525
4861
|
try {
|
|
4862
|
+
// Inspect explicit selectors with the existing parser before new-work routing,
|
|
4863
|
+
// including selectors before the command. Inherited captures are not prepare inputs.
|
|
4864
|
+
let captured;
|
|
4865
|
+
try { captured = capturedSelector(args, {}); }
|
|
4866
|
+
catch (error) {
|
|
4867
|
+
if (JSON_MODE) jsonFail(error.code || "E_BAD_ARGS", error.message);
|
|
4868
|
+
die(error.message);
|
|
4869
|
+
}
|
|
4870
|
+
if (cmd === "prepare" || captured?.args[0] === "prepare") {
|
|
4871
|
+
if (captured) {
|
|
4872
|
+
if (JSON_MODE) jsonFail("E_BAD_ARGS", "prepare is explicit new work and cannot use captured selectors");
|
|
4873
|
+
die("prepare is explicit new work and cannot use captured selectors");
|
|
4874
|
+
}
|
|
4875
|
+
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
4876
|
+
prepareCmd(); process.exit(0);
|
|
4877
|
+
}
|
|
4878
|
+
// Other commands retain their existing explicit/inherited selection rules.
|
|
4879
|
+
try { captured ??= capturedSelector(args); }
|
|
4880
|
+
catch (error) {
|
|
4881
|
+
if (JSON_MODE) jsonFail(error.code || "E_BAD_ARGS", error.message);
|
|
4882
|
+
die(error.message);
|
|
4883
|
+
}
|
|
4884
|
+
if (captured) {
|
|
4885
|
+
args.splice(0, args.length, ...captured.args); cmd = args[0];
|
|
4886
|
+
// Host protocol negotiation describes this executable, not a mutable
|
|
4887
|
+
// configuration. An inherited capture must not break `oats version` probes.
|
|
4888
|
+
if (captured.explicit || cmd !== "version") { capturedCommand(captured); process.exit(0); }
|
|
4889
|
+
}
|
|
4526
4890
|
// `--help`/`-h` anywhere after a kernel command prints that command's usage
|
|
4527
4891
|
// and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
|
|
4528
4892
|
// using a command, and `install --help` once ran the bare restore while
|
|
4529
4893
|
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
4530
|
-
const KERNEL_COMMANDS = new Set(["capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
4531
4894
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
4532
4895
|
if (cmd && KERNEL_COMMANDS.has(cmd) && wantsHelp) { if (JSON_MODE) { jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); process.exit(0); } usageFor(cmd); process.exit(0); }
|
|
4533
4896
|
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "use", "soul", "launch-config"].includes(cmd)) await serverRouteCmd();
|
|
@@ -4821,6 +5184,34 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
4821
5184
|
design, repo checkout only; see
|
|
4822
5185
|
packages/experimental/README.md
|
|
4823
5186
|
|
|
5187
|
+
oats prepare --request <absolute-json-file> [--json]
|
|
5188
|
+
complete public preparation input; no mixed flags,
|
|
5189
|
+
inherited binding, implicit setup or launch authority
|
|
5190
|
+
oats prepare --dir <abs> --source <git repo> --revision <ref> --export <path>
|
|
5191
|
+
--alias <name> [--work <mode>] [--json] prepare retained commands and curriculum,
|
|
5192
|
+
no launch; provider gaps report incomplete
|
|
5193
|
+
oats prepare --dir <abs> --workspace <git repo> --alias <advertised alias>
|
|
5194
|
+
[--workspace-revision <ref>] [--work <mode>] [--json]
|
|
5195
|
+
same preparation through workspace imports
|
|
5196
|
+
oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
|
|
5197
|
+
[--helper <exact-map-key>] inspect retained source/helper inputs, not today's configuration
|
|
5198
|
+
oats trust <capability> --deployment <abs> --resolution <id> [--json]
|
|
5199
|
+
explicitly approve that exact captured artifact
|
|
5200
|
+
oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
|
|
5201
|
+
run the approved retained command; no ambient fallback
|
|
5202
|
+
oats operation run <layer>:<name> --deployment <abs> --resolution <id>
|
|
5203
|
+
[--home <abs>] [--arg k=v ...] [--retry-intent <saved-id>] [--json]
|
|
5204
|
+
home actions admit distinct requests; explicit retry reuses intent
|
|
5205
|
+
scope views stay read-only; scope mutation is not yet qualified
|
|
5206
|
+
oats spawn <captured subject> --deployment <abs> --resolution <id>
|
|
5207
|
+
--home <abs> --no-launch [--json] create a fresh directory scaffold and run captured hooks
|
|
5208
|
+
oats session inspect --deployment <abs> --resolution <id> --home <abs> --native-record <UUID> [--helper <exact-key>] --json
|
|
5209
|
+
read-only Pi completion observation; not admission or readiness
|
|
5210
|
+
oats session start|restart --deployment <abs> --resolution <id> --home <abs>
|
|
5211
|
+
[--helper <exact-source-map-key>] [--request <abs-json>] [--retry-intent <saved-id>] [--json]
|
|
5212
|
+
dispatch the owned captured home via existing native custody;
|
|
5213
|
+
version1 request: backend/task/stopGraceMs, no model override
|
|
5214
|
+
|
|
4824
5215
|
oats <namespace> <command> [args…] run an operational command only when its
|
|
4825
5216
|
capability is active (e.g. oats okf harvest)
|
|
4826
5217
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runBindingWire } from '../lib/binding-wire.mjs';
|
|
3
|
+
|
|
4
|
+
const args=process.argv.slice(2);
|
|
5
|
+
if(args.includes('--help') || args.includes('-h')) {
|
|
6
|
+
process.stdout.write('oats okf provider binding phase (manifest-owned JSON stdin/stdout)\n');
|
|
7
|
+
} else {
|
|
8
|
+
const phase=args[0];
|
|
9
|
+
if(args.length!==1) {
|
|
10
|
+
await runBindingWire(phase,[],process.stdout);
|
|
11
|
+
} else {
|
|
12
|
+
await runBindingWire(phase);
|
|
13
|
+
}
|
|
14
|
+
}
|