@awebai/oats 0.25.9 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/bin/oats.mjs +648 -1755
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +8 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +229 -58
- package/docs/capability-manifest.schema.json +29 -9
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +18 -28
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
- package/docs/design/2026-09-24-phase-d-plan.md +57 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +604 -271
- package/docs/desktop-instance-start.md +3 -3
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +15 -18
- package/docs/implementation.md +31 -62
- package/docs/integrations.md +64 -33
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +30 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +76 -53
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/release-notes/v0.27.0.md +100 -0
- package/docs/schedules.md +54 -132
- package/docs/servers.md +4 -4
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +60 -47
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +2 -2
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +947 -5023
- package/lib/deprecation.mjs +24 -0
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +397 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/process-group.mjs +1 -1
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/remote.mjs +1 -1
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +136 -292
- package/lib/servers.mjs +70 -38
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +132 -20
- package/package-catalog.json +6 -6
- package/package.json +1 -1
- package/packages/record/lib/session-roots.mjs +8 -6
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/readiness.mjs
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/** K5 — readiness quartet, signature status and enforced policy, as DATA.
|
|
2
|
-
*
|
|
3
|
-
* `installed | trusted | configured | enrolled`, each `pass | fail | unknown |
|
|
4
|
-
* not-applicable` with items a human can act on: subject, whether it is
|
|
5
|
-
* required, the reason, the producer of the fact, evidence, remedy. Every fact
|
|
6
|
-
* is derived from what inspect already reports (capability inventory, health,
|
|
7
|
-
* executable approval, activation, runtime-package requirements, soul
|
|
8
|
-
* declarations) — never a second opinion. Unknown is unknown; "Ready" is the
|
|
9
|
-
* consumer's word and only when every required check passes. */
|
|
10
|
-
import { spawnSync } from "node:child_process";
|
|
11
|
-
import { killGroup } from "./process-group.mjs";
|
|
12
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import { parseYamlNested } from "./core.mjs";
|
|
16
|
-
|
|
17
|
-
export const READINESS_API = 1;
|
|
18
|
-
|
|
19
|
-
function roll(items, { required = (i) => i.required !== false } = {}) {
|
|
20
|
-
const req = items.filter(required);
|
|
21
|
-
if (!items.length) return "not-applicable";
|
|
22
|
-
if (req.some((i) => i.status === "fail")) return "fail";
|
|
23
|
-
if (req.some((i) => i.status === "unknown")) return "unknown";
|
|
24
|
-
if (req.every((i) => i.status === "pass" || i.status === "not-applicable")) return req.length ? "pass" : "not-applicable";
|
|
25
|
-
return "unknown";
|
|
26
|
-
}
|
|
27
|
-
const item = (subject, status, { required = true, reason = null, producer = "kernel", evidence = null, remedy = null, ...rest } = {}) =>
|
|
28
|
-
({ subject, status, required, reason, producer, evidence, remedy, ...rest });
|
|
29
|
-
|
|
30
|
-
/** Verified Git signature of a source commit, named signer or nothing.
|
|
31
|
-
* Requires network (fetch) — only when the caller asks (`verify: true`);
|
|
32
|
-
* otherwise `unknown` with the reason. Never a URL, owner or hash as signer. */
|
|
33
|
-
export const SIGNATURE_FAILURES = Object.freeze(["transport-not-allowed", "fetch-failed", "fetch-timeout", "budget-exhausted", "verifier-failed", "verifier-timeout", "cannot-check", "no-source"]);
|
|
34
|
-
const ALLOWED_TRANSPORTS = /^(https:\/\/|ssh:\/\/|git@[^/:]+:)/;
|
|
35
|
-
/** Verified Git signature of a source commit, named signer or nothing.
|
|
36
|
-
* Requires network (fetch) — only when the caller asks (`verify: true`);
|
|
37
|
-
* otherwise `unknown` with the reason. Never a URL, owner or hash as signer.
|
|
38
|
-
* Bounded custody: one total budget per call (`budgetMs`, default 60s) shared
|
|
39
|
-
* by fetch and verify; each Git child runs in its own process group and is
|
|
40
|
-
* killed with the group on timeout; the scratch repository is removed on every
|
|
41
|
-
* exit including signals; Git reads NO global/system config and cannot prompt;
|
|
42
|
-
* only https/ssh transports are fetched. Failures carry a closed `failure`
|
|
43
|
-
* code — never stderr. */
|
|
44
|
-
/** One verification budget for a whole readiness read: every signatureOf call
|
|
45
|
-
* in that read draws from it, so a target with N capabilities is bounded by the
|
|
46
|
-
* TOTAL (default 120 s), not N × per-call. */
|
|
47
|
-
export function verificationBudget(totalMs = 120000) {
|
|
48
|
-
const started = Date.now();
|
|
49
|
-
return { totalMs, remaining: () => totalMs - (Date.now() - started) };
|
|
50
|
-
}
|
|
51
|
-
const scratchDirs = new Set();
|
|
52
|
-
let signalCleanupInstalled = false;
|
|
53
|
-
function installSignalCleanup() {
|
|
54
|
-
if (signalCleanupInstalled) return; signalCleanupInstalled = true;
|
|
55
|
-
const sweep = () => { for (const d of scratchDirs) { try { rmSync(d, { recursive: true, force: true }); } catch { /* best effort */ } } scratchDirs.clear(); };
|
|
56
|
-
process.once("exit", sweep);
|
|
57
|
-
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => { sweep(); process.exit(128 + (sig === "SIGINT" ? 2 : sig === "SIGTERM" ? 15 : 1)); });
|
|
58
|
-
}
|
|
59
|
-
export function signatureOf({ url, commit }, { verify = false, budgetMs = 60000, budget = null } = {}) {
|
|
60
|
-
if (!url || !commit || commit === "local") return { status: "not-applicable", signer: null, reason: commit === "local" ? "path-installed capability has no source commit" : "no source recorded", failure: null };
|
|
61
|
-
if (!verify) return { status: "unknown", signer: null, reason: "signature verification needs a network fetch; pass --verify-signatures", failure: null };
|
|
62
|
-
if (!ALLOWED_TRANSPORTS.test(url)) return { status: "unknown", signer: null, reason: "source transport is not https or ssh; not fetched", failure: { code: "transport-not-allowed" } };
|
|
63
|
-
const shared = budget ?? verificationBudget(budgetMs);
|
|
64
|
-
if (shared.remaining() <= 0) return { status: "unknown", signer: null, reason: "the verification budget was exhausted", failure: { code: "budget-exhausted" } };
|
|
65
|
-
installSignalCleanup();
|
|
66
|
-
const dir = mkdtempSync(join(tmpdir(), "oats-sig-")); scratchDirs.add(dir);
|
|
67
|
-
const cleanup = () => { scratchDirs.delete(dir); try { rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ } };
|
|
68
|
-
const env = { PATH: process.env.PATH ?? "", HOME: dir, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_ASKPASS: "/bin/false", SSH_ASKPASS: "/bin/false", GIT_SSH_COMMAND: "ssh -o BatchMode=yes", LC_ALL: "C" };
|
|
69
|
-
const git = (args, stage) => {
|
|
70
|
-
const left = shared.remaining();
|
|
71
|
-
if (left <= 0) throw Object.assign(new Error("budget"), { failure: "budget-exhausted" });
|
|
72
|
-
const child = spawnSync("git", ["-C", dir, "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "protocol.allow=never", "-c", "protocol.https.allow=always", "-c", "protocol.ssh.allow=always", ...args],
|
|
73
|
-
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: left, killSignal: "SIGKILL", detached: true, env, shell: false });
|
|
74
|
-
if (child.error?.code === "ETIMEDOUT" || child.signal === "SIGKILL") { killGroup(child); throw Object.assign(new Error(stage), { failure: `${stage}-timeout` }); }
|
|
75
|
-
if (child.error || child.status !== 0) { killGroup(child); throw Object.assign(new Error(stage), { failure: `${stage}-failed` }); }
|
|
76
|
-
return child.stdout;
|
|
77
|
-
};
|
|
78
|
-
try {
|
|
79
|
-
git(["init", "-q"], "verifier");
|
|
80
|
-
git(["fetch", "-q", "--depth", "1", url, commit], "fetch");
|
|
81
|
-
// %G? : G good, B bad, U good-untrusted, X expired, Y expired key, R revoked, E cannot check, N none
|
|
82
|
-
const out = git(["log", "-1", "--format=%G?%x00%GS%x00%GK%x00%GF", commit], "verifier").trim();
|
|
83
|
-
const [code, signerName, keyId, fingerprint] = out.split("\0");
|
|
84
|
-
if (code === "N") return { status: "unsigned", signer: null, reason: "commit carries no signature", failure: null };
|
|
85
|
-
if (code === "G") return { status: "verified", signer: { id: fingerprint || keyId || null, label: signerName || null }, reason: null, failure: null };
|
|
86
|
-
if (code === "U") return { status: "verified", signer: { id: fingerprint || keyId || null, label: signerName || null }, reason: "good signature from a key not marked trusted in the local keyring", trust: "untrusted-key", failure: null };
|
|
87
|
-
if (code === "E") return { status: "unknown", signer: null, reason: "signature present but cannot be checked (missing public key)", failure: { code: "cannot-check" } };
|
|
88
|
-
return { status: "invalid", signer: null, reason: { B: "bad signature", X: "good signature that has expired", Y: "good signature made by an expired key", R: "good signature made by a revoked key" }[code] || "verifier reported an unrecognised state", failure: null };
|
|
89
|
-
} catch (e) {
|
|
90
|
-
const code = SIGNATURE_FAILURES.includes(e.failure) ? e.failure : "verifier-failed";
|
|
91
|
-
const reasons = { "fetch-failed": "the source could not be fetched", "fetch-timeout": "the fetch exceeded the verification budget", "budget-exhausted": "the verification budget was exhausted", "verifier-failed": "the local verifier failed", "verifier-timeout": "the local verifier exceeded the verification budget" };
|
|
92
|
-
return { status: "unknown", signer: null, reason: reasons[code], failure: { code } };
|
|
93
|
-
} finally { cleanup(); }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function sourceOfCapability(cap, catalog) {
|
|
97
|
-
// cap.source is e.g. "catalog:oats.framework" or "git:https://…@ref#path"; the
|
|
98
|
-
// installed record's package commit is what we would verify.
|
|
99
|
-
const src = String(cap.source || "");
|
|
100
|
-
if (src.startsWith("git:")) { const m = /^git:([^@#]+)(?:@([^#]+))?/.exec(src); return { url: m?.[1] ?? null, commit: cap.commit ?? m?.[2] ?? null }; }
|
|
101
|
-
if (src.startsWith("catalog:") && catalog) { const entry = catalog.packages?.[src.slice(8)]; return { url: entry?.url ?? null, commit: cap.commit ?? null }; }
|
|
102
|
-
return { url: null, commit: cap.commit ?? null };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** The quartet for a scope or one soul, from an inspect result. */
|
|
106
|
-
export function readinessOf(inspect, { soul = null, verifySignatures = false, catalog = null, deploymentDir = null, memberDocument = null, selector = null, verificationBudgetMs = 120000 } = {}) {
|
|
107
|
-
const caps = inspect.capabilities || [];
|
|
108
|
-
const budget = verifySignatures ? verificationBudget(verificationBudgetMs) : null;
|
|
109
|
-
const soulEntry = soul ? (inspect.souls || []).find((s) => s.name === soul) : null;
|
|
110
|
-
const declaredRequires = soulEntry?.declarations?.requires?.capabilities ? Object.keys(soulEntry.declarations.requires.capabilities) : null;
|
|
111
|
-
const required = new Set(declaredRequires ?? caps.filter((c) => c.activation?.enabled).map((c) => c.id));
|
|
112
|
-
const declaredForSoul = (c) => !!c.activation?.declaredAt?.some((d) => (d.targets || []).includes(`soul:${soul}`));
|
|
113
|
-
const relevant = soul ? caps.filter((c) => required.has(c.id) || declaredForSoul(c)) : caps;
|
|
114
|
-
// Typed linkage for consumers (frame-level per-capability rows): WHICH
|
|
115
|
-
// capability, at which config level/scope, and WHY it is in this quartet.
|
|
116
|
-
const capabilityOf = (c) => ({ id: c.id, level: c.activation?.level ?? null, scope: c.activation?.target ?? null });
|
|
117
|
-
const originOf = (c) => declaredRequires?.includes(c.id) ? { kind: "requires", target: `soul:${soul}` }
|
|
118
|
-
: soul && declaredForSoul(c) ? { kind: "declares", target: `soul:${soul}` }
|
|
119
|
-
: c.activation?.enabled ? { kind: "default", target: c.activation?.target ?? "global" } : { kind: "inventory", target: null };
|
|
120
|
-
const typed = (c) => ({ capability: capabilityOf(c), origin: originOf(c) });
|
|
121
|
-
|
|
122
|
-
// installed — the artifact's bytes are present and locked with matching integrity
|
|
123
|
-
const installed = relevant.map((c) => {
|
|
124
|
-
const ok = c.health?.installed === true && (c.health.integrity == null || c.health.installedIntegrity == null || c.health.integrity === c.health.installedIntegrity);
|
|
125
|
-
return item(c.id, ok ? "pass" : c.health?.installed === false ? "fail" : "unknown", { required: required.has(c.id), producer: "oats list",
|
|
126
|
-
reason: ok ? null : c.health?.installed === false ? "not acquired" : c.health?.code || "integrity drift", evidence: { version: c.version ?? null, integrity: c.health?.integrity ?? null, origin: c.origin ?? null },
|
|
127
|
-
remedy: ok ? null : `oats install ${c.package || c.id}`, ...typed(c) });
|
|
128
|
-
});
|
|
129
|
-
for (const id of required) if (!caps.some((c) => c.id === id)) installed.push(item(id, "fail", { producer: "soul declaration", reason: "declared by the soul but not in the inventory", remedy: `oats install <package providing ${id}>`,
|
|
130
|
-
capability: { id, level: null, scope: null }, origin: { kind: "requires", target: `soul:${soul}` } }));
|
|
131
|
-
|
|
132
|
-
// trusted — executable approval of the exact artifact; signature separately
|
|
133
|
-
const trusted = relevant.map((c) => {
|
|
134
|
-
// The inspect row states whether the manifest has anything trust approves
|
|
135
|
-
// (commands/hooks/launch env). No surface → trust is not-applicable, however
|
|
136
|
-
// the lock records it. Older rows without the flag fall back to the old heuristic.
|
|
137
|
-
const executable = typeof c.health?.executableSurface === "boolean" ? c.health.executableSurface
|
|
138
|
-
: !!(c.operations?.length || c.health?.code === "untrusted-surface");
|
|
139
|
-
const approved = c.health?.trusted === true;
|
|
140
|
-
const sig = signatureOf(sourceOfCapability(c, catalog), { verify: verifySignatures, budget });
|
|
141
|
-
return item(c.id, !executable ? "not-applicable" : approved ? "pass" : c.health?.trusted === false ? "fail" : "unknown", { required: required.has(c.id), producer: "artifact approval",
|
|
142
|
-
reason: !executable ? "no executable surface" : approved ? null : "executable surface not approved", evidence: { integrity: c.health?.integrity ?? null }, remedy: approved || !executable ? null : `oats trust ${c.id}`,
|
|
143
|
-
signature: sig, ...typed(c) });
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// configured — EFFECTIVE activation for the subject (a declaration at the
|
|
147
|
-
// soul is not activation: `enabled` is the resolved verdict for this subject,
|
|
148
|
-
// and a declared-but-disabled binding is a fail that says so) + runtime
|
|
149
|
-
// package requirements + layer readiness problems
|
|
150
|
-
const configured = [];
|
|
151
|
-
for (const c of relevant) {
|
|
152
|
-
const active = c.activation?.enabled === true;
|
|
153
|
-
const declaredOnly = !active && soul && declaredForSoul(c);
|
|
154
|
-
if (required.has(c.id)) configured.push(item(`${c.id} activation`, active ? "pass" : "fail", { producer: "oats-config.yaml",
|
|
155
|
-
reason: active ? null : declaredOnly ? `declared for soul ${soul} but disabled${c.activation?.reason ? ` (${c.activation.reason})` : ""}` : `not active for ${soul ? `soul ${soul}` : "this scope"}`,
|
|
156
|
-
evidence: { target: c.activation?.target ?? null, level: c.activation?.level ?? null, source: c.activation?.source ?? null }, remedy: active ? null : `oats use ${c.id}${soul ? ` --soul ${soul}` : ""}`, ...typed(c) }));
|
|
157
|
-
for (const miss of c.missingRequires || []) configured.push(item(`${c.id} requires ${miss.command}`, "fail", { producer: "capability manifest", reason: miss.why || "required command not on PATH", remedy: miss.install || null, ...typed(c) }));
|
|
158
|
-
}
|
|
159
|
-
for (const p of inspect.problems || []) if (/runtime package|DISABLED|extension/i.test(p.message || "")) configured.push(item(p.capability || p.code, "fail", { producer: "runtime settings", reason: p.message, remedy: null }));
|
|
160
|
-
if (soulEntry && soulEntry.readiness?.status === "undeclared") configured.push(item(`${soul} declarations`, "not-applicable", { required: false, producer: "soul.yaml", reason: "no requirements declared" }));
|
|
161
|
-
|
|
162
|
-
// enrolled — workspace member admission (decision §3): not-applicable for
|
|
163
|
-
// standalone; pass/fail when this repository declares a workspace.
|
|
164
|
-
const enrolled = [];
|
|
165
|
-
const member = memberDocument ?? readMemberDocument(deploymentDir);
|
|
166
|
-
if (!member) enrolled.push(item("workspace membership", "not-applicable", { required: false, producer: "oats.yaml", reason: "standalone deployment: no workspace declared in oats.yaml" }));
|
|
167
|
-
else if (member.unreadable) enrolled.push(item("workspace membership", "unknown", { producer: "oats.yaml", reason: "workspace member document is unreadable; membership cannot be stated", evidence: { file: member.file ?? null }, remedy: "repair oats.yaml (valid YAML) and re-run" }));
|
|
168
|
-
else if (!member.workspace?.source) enrolled.push(item("workspace membership", "not-applicable", { required: false, producer: "oats.yaml", reason: "oats.yaml declares exports but no workspace backlink" }));
|
|
169
|
-
else enrolled.push(item(`member of ${member.workspace.source}`, member.admitted === true ? "pass" : member.admitted === false ? "fail" : "unknown", { producer: "workspace discovery",
|
|
170
|
-
reason: member.admitted === true ? null : member.admitted === false ? "this repository is not admitted in the workspace's members" : "reciprocal admission not observed: this CLI reads the backlink but does not yet fetch the workspace's members (declared, not verified)",
|
|
171
|
-
evidence: { workspace: member.workspace.source, revision: member.workspace.revision ?? null }, remedy: member.admitted === true ? null : "ask the workspace maintainer to admit this repository (oats-workspace.yaml members) — enrolment is admission, not login" }));
|
|
172
|
-
|
|
173
|
-
const checks = { installed: { status: roll(installed), items: installed }, trusted: { status: roll(trusted), items: trusted }, configured: { status: roll(configured), items: configured }, enrolled: { status: roll(enrolled), items: enrolled } };
|
|
174
|
-
const requiredStatuses = Object.values(checks).flatMap((c) => c.items.filter((i) => i.required).map((i) => i.status));
|
|
175
|
-
// Per-capability grouping of the same items (no second observation): each
|
|
176
|
-
// capability's four verdicts, ready only if all its REQUIRED items pass.
|
|
177
|
-
const byCapability = [...new Set(Object.values(checks).flatMap((c) => c.items.map((i) => i.capability?.id).filter(Boolean)))].sort().map((id) => {
|
|
178
|
-
const of = (name) => checks[name].items.filter((i) => i.capability?.id === id);
|
|
179
|
-
const statuses = Object.keys(checks).flatMap((name) => of(name).filter((i) => i.required).map((i) => i.status));
|
|
180
|
-
const any = Object.keys(checks).flatMap((name) => of(name)).find(Boolean);
|
|
181
|
-
return { capability: any?.capability ?? { id, level: null, scope: null }, origin: any?.origin ?? null, required: statuses.length > 0,
|
|
182
|
-
checks: Object.fromEntries(Object.keys(checks).map((name) => [name, of(name).length ? roll(of(name)) : "not-applicable"])),
|
|
183
|
-
ownReady: statuses.length > 0 && statuses.every((s) => s === "pass" || s === "not-applicable") };
|
|
184
|
-
});
|
|
185
|
-
// Items that belong to no capability (workspace membership, soul declarations)
|
|
186
|
-
// are SUBJECT-level: a capability's own verdict never overrides them, and a
|
|
187
|
-
// row must not read "ready" while the subject is blocked by one of them.
|
|
188
|
-
const subjectBlockers = Object.entries(checks).flatMap(([name, c]) => c.items.filter((i) => i.required && !i.capability && i.status !== "pass" && i.status !== "not-applicable").map((i) => ({ check: name, subject: i.subject, status: i.status })));
|
|
189
|
-
for (const g of byCapability) g.ready = g.ownReady && subjectBlockers.length === 0;
|
|
190
|
-
return { readinessApi: READINESS_API, subject: { ...(soul ? { kind: "soul", name: soul } : { kind: "scope", context: inspect.scope?.context ?? null }), ...(selector ? { selector } : {}) }, at: new Date().toISOString(),
|
|
191
|
-
checks, summary: { ready: requiredStatuses.length > 0 && requiredStatuses.every((s) => s === "pass" || s === "not-applicable"), required: requiredStatuses.length,
|
|
192
|
-
pass: requiredStatuses.filter((s) => s === "pass").length, fail: requiredStatuses.filter((s) => s === "fail").length, unknown: requiredStatuses.filter((s) => s === "unknown").length, byCapability, subjectBlockers },
|
|
193
|
-
notes: [
|
|
194
|
-
...(verifySignatures ? [] : ["signatures are unknown until --verify-signatures (network fetch)"]),
|
|
195
|
-
"ready means every REQUIRED check passes; it is never inferred from an empty set",
|
|
196
|
-
"enrolment is workspace member admission, not native login or team registration",
|
|
197
|
-
] };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function readMemberDocument(deploymentDir) {
|
|
201
|
-
if (!deploymentDir) return null;
|
|
202
|
-
const file = join(deploymentDir, "oats.yaml");
|
|
203
|
-
if (!existsSync(file)) return null;
|
|
204
|
-
try {
|
|
205
|
-
const doc = parseYamlNested(readFileSync(file, "utf8"));
|
|
206
|
-
// The lenient parser never throws: a `workspace:` that is present but not a
|
|
207
|
-
// mapping is a document we cannot read a membership from — unreadable, not absent.
|
|
208
|
-
if (doc.workspace !== undefined && (doc.workspace === null || typeof doc.workspace !== "object")) return { workspace: null, admitted: null, unreadable: true, file };
|
|
209
|
-
return { workspace: doc.workspace && typeof doc.workspace === "object" ? doc.workspace : null, admitted: null, exports: doc.exports ?? null };
|
|
210
|
-
} catch { return { workspace: null, admitted: null, unreadable: true, file }; }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Enforced policy for an instance (from its recorded metadata) or a soul
|
|
214
|
-
* (its declaration + default), with origins. Advisory/unknown stays unknown. */
|
|
215
|
-
export function policyOf({ instanceMeta = null, soul = null } = {}) {
|
|
216
|
-
const child = instanceMeta?.policy?.childSpawns
|
|
217
|
-
? { allowed: instanceMeta.policy.childSpawns.allowed === true, enforced: true, origin: instanceMeta.policy.childSpawns.origin ?? { kind: "recorded" } }
|
|
218
|
-
: instanceMeta ? { allowed: true, enforced: true, origin: { kind: "default", detail: "no recorded policy: children allowed (pre-0.24.8 instance)" } }
|
|
219
|
-
: soul?.declarations?.children && typeof soul.declarations.children.spawn === "boolean"
|
|
220
|
-
? { allowed: soul.declarations.children.spawn, enforced: false, origin: { kind: "soul", detail: "children.spawn in soul.yaml; enforced once an instance records it" } }
|
|
221
|
-
: { allowed: true, enforced: false, origin: { kind: "default", detail: "no declaration: children allowed" } };
|
|
222
|
-
const worktree = instanceMeta ? { allowed: instanceMeta.work === "worktree" || instanceMeta.work === "checkout", mode: instanceMeta.work ?? null, enforced: true, origin: { kind: "work-mode", detail: `work: ${instanceMeta.work}` } }
|
|
223
|
-
: soul ? { allowed: ["worktree", "checkout"].includes(soul.work), mode: soul.work ?? null, enforced: false, origin: { kind: "soul", detail: `work: ${soul.work}` } } : { allowed: null, mode: null, enforced: false, origin: { kind: "unknown" } };
|
|
224
|
-
return { readinessApi: READINESS_API, policy: { childSpawns: child, worktrees: worktree }, notes: ["a lifecycle-authority claim enforced by the spawn route, not an OS sandbox"] };
|
|
225
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/** Exact, transaction-scoped repository observations. Bare Git reads never check
|
|
2
|
-
* out or execute source content. Cache lifetime/auth context is one transaction. */
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
5
|
-
import { isAbsolute, join } from "node:path";
|
|
6
|
-
import { canonicalJson, decodeUtf8, freezeJson, parseStrictJson } from "./portable-values.mjs";
|
|
7
|
-
import { bytesIntegrity } from "./portable-digest.mjs";
|
|
8
|
-
import { parseRepositorySource, portablePath, revisionSelector } from "./source-spec.mjs";
|
|
9
|
-
import { sameIdentity, validateRepositoryIdentity } from "./portable-identity.mjs";
|
|
10
|
-
import { validateOrigin } from "./resolution-shape.mjs";
|
|
11
|
-
import { createSourceProjection, validateProjectionPath } from "./source-projection.mjs";
|
|
12
|
-
export { validateProjectionPath } from "./source-projection.mjs";
|
|
13
|
-
import { oatsError } from "./errors.mjs";
|
|
14
|
-
|
|
15
|
-
function githubCoordinates(url) {
|
|
16
|
-
let host, path;
|
|
17
|
-
if (url.includes("://")) { const parsed = new URL(url); host = parsed.hostname; path = parsed.pathname.slice(1); }
|
|
18
|
-
else { const match = /^[^@]+@([^:]+):(.+)$/.exec(url); if (!match) return null; [, host, path] = match; }
|
|
19
|
-
if (host.toLowerCase() !== "github.com") return null;
|
|
20
|
-
const parts = path.replace(/\.git$/, "").split("/");
|
|
21
|
-
if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9_.-]+$/.test(part))) throw oatsError("source-identity-unresolved", "GitHub repository locator cannot be qualified");
|
|
22
|
-
return { host: "github.com", owner: parts[0], name: parts[1] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Native host credentials only; no token extraction, copying or fallback after
|
|
26
|
-
* a GitHub identity lookup fails. Unsupported hosts use explicit remote identity. */
|
|
27
|
-
export function readHostingIdentity(repository, { environment = process.env, expectedIdentity } = {}) {
|
|
28
|
-
if (expectedIdentity?.kind === "canonical-remote") return { identity: { kind: "canonical-remote", remote: repository.normalized } };
|
|
29
|
-
const coordinates = githubCoordinates(repository.url);
|
|
30
|
-
if (!coordinates) return { identity: { kind: "canonical-remote", remote: repository.normalized } };
|
|
31
|
-
let metadata;
|
|
32
|
-
try {
|
|
33
|
-
const output = execFileSync("gh", ["api", "--hostname", coordinates.host, `repos/${coordinates.owner}/${coordinates.name}`,
|
|
34
|
-
"--jq", "{id, full_name, default_branch}"], { env: environment, timeout: 30_000, maxBuffer: 64 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
35
|
-
metadata = parseStrictJson(output);
|
|
36
|
-
} catch { throw oatsError("source-identity-unresolved", "repository hosting identity is unavailable in the selected access context"); }
|
|
37
|
-
if (!Number.isSafeInteger(metadata.id) || metadata.id < 1 || typeof metadata.full_name !== "string") throw oatsError("source-identity-unresolved", "hosting response lacks a stable repository identity");
|
|
38
|
-
if (metadata.full_name.toLowerCase() !== `${coordinates.owner}/${coordinates.name}`.toLowerCase()) throw oatsError("source-identity-change", "repository redirect or rename requires an explicit identity mapping");
|
|
39
|
-
revisionSelector(metadata.default_branch);
|
|
40
|
-
return { identity: { kind: "provider-repository", provider: "github", host: coordinates.host, id: String(metadata.id) }, defaultBranch: metadata.default_branch };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function gitEnvironment(environment) {
|
|
44
|
-
const out = { ...environment };
|
|
45
|
-
// Keep native credential/SSH facilities, but never an invoking repository's
|
|
46
|
-
// object/index/config injection. Host HOME/config credentials remain host-owned.
|
|
47
|
-
const retained = new Set(["GIT_ASKPASS", "GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_CONFIG_NOSYSTEM"]);
|
|
48
|
-
for (const key of Object.keys(out)) if (key.startsWith("GIT_") && !retained.has(key)) delete out[key];
|
|
49
|
-
out.GIT_TERMINAL_PROMPT = "0"; out.GIT_LITERAL_PATHSPECS = "1"; out.GIT_NO_REPLACE_OBJECTS = "1";
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function createRepositoryTransaction({ directory, accessContextKey, environment = process.env, identityReader = readHostingIdentity, allowLocalGit = false,
|
|
54
|
-
maxRepositories = 32, maxEntries = 10_000, maxBytes = 256 * 1024 * 1024 } = {}) {
|
|
55
|
-
if (typeof directory !== "string" || !isAbsolute(directory) || typeof accessContextKey !== "string" || !accessContextKey) throw oatsError("invalid-source", "repository observation requires an explicit scratch directory and access context");
|
|
56
|
-
canonicalJson(accessContextKey, { maxBytes: 8192 });
|
|
57
|
-
for (const value of [maxRepositories, maxEntries, maxBytes]) if (!Number.isSafeInteger(value) || value < 1) throw oatsError("resource-limit", "invalid repository observation budget");
|
|
58
|
-
const parent = realpathSync(directory);
|
|
59
|
-
if (!lstatSync(parent).isDirectory()) throw oatsError("invalid-source", "repository observation scratch root is not a directory");
|
|
60
|
-
const root = mkdtempSync(join(parent, ".repository-transaction-")), owned = lstatSync(root), hooks = join(root, "empty-hooks");
|
|
61
|
-
mkdirSync(hooks, { mode: 0o700 });
|
|
62
|
-
const env = gitEnvironment(environment), memo = new Map(), snapshots = new Map(), hosts = new Map(), defaults = new Map(), handles = new WeakMap(), blobs = new Map();
|
|
63
|
-
let closed = false, readBytes = 0, entriesRead = 0;
|
|
64
|
-
const active = () => { if (closed) throw oatsError("source-unavailable", "repository transaction is closed"); };
|
|
65
|
-
const git = (args, limit = 1024 * 1024) => {
|
|
66
|
-
active();
|
|
67
|
-
try { return execFileSync("git", ["-c", `core.hooksPath=${hooks}`, "-c", "protocol.ext.allow=never", "-c", `protocol.file.allow=${allowLocalGit ? "always" : "never"}`,
|
|
68
|
-
"-c", "http.followRedirects=false", ...args], { cwd: root, env, timeout: 60_000, maxBuffer: limit, stdio: ["ignore", "pipe", "pipe"] }); }
|
|
69
|
-
catch (error) { throw oatsError(error.code === "ENOBUFS" ? "resource-limit" : "source-unavailable", "repository read failed in the selected access context"); }
|
|
70
|
-
};
|
|
71
|
-
const hostFor = (repository, expectedIdentity) => {
|
|
72
|
-
if (!hosts.has(repository.normalized)) {
|
|
73
|
-
if (hosts.size >= maxRepositories * 4) throw oatsError("resource-limit", "repository identity count exceeded");
|
|
74
|
-
try {
|
|
75
|
-
const response = identityReader(repository, { environment: env, expectedIdentity });
|
|
76
|
-
validateRepositoryIdentity(response.identity);
|
|
77
|
-
if (response.defaultBranch !== undefined) revisionSelector(response.defaultBranch);
|
|
78
|
-
hosts.set(repository.normalized, { value: freezeJson({ identity: structuredClone(response.identity),
|
|
79
|
-
...(response.defaultBranch === undefined ? {} : { defaultBranch: response.defaultBranch }) }) });
|
|
80
|
-
} catch (error) { hosts.set(repository.normalized, { error }); }
|
|
81
|
-
}
|
|
82
|
-
const hosted = hosts.get(repository.normalized);
|
|
83
|
-
if (hosted.error) throw hosted.error;
|
|
84
|
-
const host = hosted.value;
|
|
85
|
-
if (host.identity.kind === "canonical-remote" && host.identity.remote !== repository.normalized) throw oatsError("source-identity-change", "canonical repository identity differs from its source");
|
|
86
|
-
if (expectedIdentity && !sameIdentity(host.identity, expectedIdentity)) throw oatsError("source-identity-change", "repository hosting identity changed");
|
|
87
|
-
const identityKey = canonicalJson(host.identity);
|
|
88
|
-
if (host.defaultBranch !== undefined && !defaults.has(identityKey)) defaults.set(identityKey, { selector: host.defaultBranch });
|
|
89
|
-
return host;
|
|
90
|
-
};
|
|
91
|
-
const dataFor = (observation) => {
|
|
92
|
-
active(); const data = handles.get(observation);
|
|
93
|
-
if (!data) throw oatsError("invalid-source", "repository observation was not issued by this transaction");
|
|
94
|
-
return data;
|
|
95
|
-
};
|
|
96
|
-
const list = (data, paths, recursive) => {
|
|
97
|
-
const args = ["-C", data.directory, "ls-tree", "-z", "--full-tree", ...(recursive ? ["-r"] : []), data.commit];
|
|
98
|
-
if (!paths.includes(".")) args.push("--", ...paths);
|
|
99
|
-
const rows = decodeUtf8(git(args, 16 * 1024 * 1024)).split("\0").filter(Boolean);
|
|
100
|
-
if ((entriesRead += rows.length) > maxEntries) throw oatsError("resource-limit", "repository entry budget exceeded");
|
|
101
|
-
return rows.map((row) => {
|
|
102
|
-
const at = row.indexOf("\t"), header = /^([0-7]{6}) (blob|tree|commit) ([a-f0-9]{40})$/.exec(row.slice(0, at)), path = row.slice(at + 1);
|
|
103
|
-
if (!header || at < 0) throw oatsError("invalid-source", "repository returned a malformed tree entry");
|
|
104
|
-
portablePath(path);
|
|
105
|
-
if (path.split("/").some((part) => part.toLowerCase() === ".git")) throw oatsError("resource-not-contained", "source projection cannot include Git administrative files");
|
|
106
|
-
if (!paths.some((root) => root === "." || path === root || path.startsWith(`${root}/`))) throw oatsError("resource-not-contained", "repository returned an unselected source path");
|
|
107
|
-
return { mode: header[1], type: header[2], object: header[3], path };
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
const blob = (data, entry, limit) => {
|
|
111
|
-
if (entry.type !== "blob") throw oatsError("source-incomplete", "selected source contains an unsupported submodule or object kind");
|
|
112
|
-
const key = `${data.directory}:${entry.object}`;
|
|
113
|
-
if (!blobs.has(key)) {
|
|
114
|
-
const sizeText = decodeUtf8(git(["-C", data.directory, "cat-file", "-s", entry.object], 1024)).trim();
|
|
115
|
-
const size = Number(sizeText);
|
|
116
|
-
if (!/^[0-9]+$/.test(sizeText) || !Number.isSafeInteger(size) || size > limit || readBytes + size > maxBytes) throw oatsError("resource-limit", "repository content byte budget exceeded");
|
|
117
|
-
const bytes = git(["-C", data.directory, "cat-file", "blob", entry.object], size + 1024);
|
|
118
|
-
if (bytes.length !== size) throw oatsError("integrity-drift", "repository blob changed during reading");
|
|
119
|
-
readBytes += size; blobs.set(key, bytes);
|
|
120
|
-
}
|
|
121
|
-
const bytes = blobs.get(key);
|
|
122
|
-
if (bytes.length > limit) throw oatsError("resource-limit", "repository descriptor byte limit exceeded");
|
|
123
|
-
return Buffer.from(bytes);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
identify(source, { expectedIdentity } = {}) {
|
|
128
|
-
active();
|
|
129
|
-
if (expectedIdentity !== undefined) validateRepositoryIdentity(expectedIdentity);
|
|
130
|
-
const repository = parseRepositorySource(source), host = hostFor(repository, expectedIdentity);
|
|
131
|
-
return Object.freeze({ identity: host.identity, source: repository.normalized, accessContextKey });
|
|
132
|
-
},
|
|
133
|
-
observe(source, { revision, origin, expectedIdentity } = {}) {
|
|
134
|
-
active(); validateOrigin(origin);
|
|
135
|
-
if (expectedIdentity !== undefined) validateRepositoryIdentity(expectedIdentity);
|
|
136
|
-
// Local working snapshots use path: custody, not portable file: identities.
|
|
137
|
-
// allowLocalGit only enables an explicit host/test transport mapping.
|
|
138
|
-
const repository = parseRepositorySource(source);
|
|
139
|
-
if (revision !== undefined) revisionSelector(revision);
|
|
140
|
-
const key = canonicalJson({ source: repository.normalized, selector: revision ?? null });
|
|
141
|
-
if (!memo.has(key)) {
|
|
142
|
-
if (memo.size >= maxRepositories * 4) throw oatsError("resource-limit", "repository request count exceeded");
|
|
143
|
-
// Cache failures too: a single transaction never re-observes a moving
|
|
144
|
-
// request because its first attempt was inconvenient or inaccessible.
|
|
145
|
-
const slot = {}; memo.set(key, slot);
|
|
146
|
-
try {
|
|
147
|
-
const host = hostFor(repository, expectedIdentity), identityKey = canonicalJson(host.identity);
|
|
148
|
-
let selector = revision ?? defaults.get(identityKey)?.selector, exactHead = revision === undefined ? defaults.get(identityKey)?.exactHead : undefined;
|
|
149
|
-
if (selector === undefined) {
|
|
150
|
-
const head = decodeUtf8(git(["ls-remote", "--symref", "--", repository.url, "HEAD"], 64 * 1024));
|
|
151
|
-
const symbolic = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(head), resolved = /^([a-f0-9]{40})\tHEAD$/m.exec(head);
|
|
152
|
-
if (!symbolic || !resolved) throw oatsError("source-identity-unresolved", "repository default branch is unavailable");
|
|
153
|
-
selector = symbolic[1]; exactHead = resolved[1];
|
|
154
|
-
defaults.set(identityKey, { selector, exactHead });
|
|
155
|
-
}
|
|
156
|
-
revisionSelector(selector);
|
|
157
|
-
const snapshotKey = canonicalJson({ identity: host.identity, selector });
|
|
158
|
-
if (snapshots.has(snapshotKey)) {
|
|
159
|
-
const existing = snapshots.get(snapshotKey);
|
|
160
|
-
if (existing.error) throw existing.error;
|
|
161
|
-
slot.data = existing.data;
|
|
162
|
-
} else {
|
|
163
|
-
if (snapshots.size >= maxRepositories) throw oatsError("resource-limit", "repository snapshot count exceeded");
|
|
164
|
-
const directory = join(root, `repository-${snapshots.size}.git`);
|
|
165
|
-
snapshots.set(snapshotKey, slot);
|
|
166
|
-
// One selected shallow snapshot, not all repository branches/history.
|
|
167
|
-
// Servers may decline filtering; budgets below bound selected reads,
|
|
168
|
-
// not the transport's pack size. Git/host resource limits still apply.
|
|
169
|
-
git(["init", "--quiet", "--bare", directory]);
|
|
170
|
-
git(["-C", directory, "remote", "add", "origin", repository.url]);
|
|
171
|
-
git(["-C", directory, "fetch", "--quiet", "--depth=1", "--filter=blob:none", "--no-tags", "--", "origin", exactHead ?? selector], 4 * 1024 * 1024);
|
|
172
|
-
const commit = decodeUtf8(git(["-C", directory, "rev-parse", "--verify", "--quiet", "--end-of-options", "FETCH_HEAD^{commit}"], 1024)).trim();
|
|
173
|
-
if (exactHead && commit !== exactHead) throw oatsError("integrity-drift", "fetched default branch differs from its exact observation");
|
|
174
|
-
if (!/^[a-f0-9]{40}$/.test(commit)) throw oatsError("source-identity-unresolved", "repository selector did not resolve to an exact commit");
|
|
175
|
-
slot.data = { directory, identity: Object.freeze({ ...host.identity }), selector, commit };
|
|
176
|
-
}
|
|
177
|
-
} catch (error) { slot.error = error; throw error; }
|
|
178
|
-
}
|
|
179
|
-
const slot = memo.get(key);
|
|
180
|
-
if (slot.error) throw slot.error;
|
|
181
|
-
const data = { ...slot.data, remote: repository.normalized };
|
|
182
|
-
if (expectedIdentity && !sameIdentity(data.identity, expectedIdentity)) throw oatsError("source-identity-change", "cached repository identity differs from the request");
|
|
183
|
-
const observation = Object.freeze({ schemaVersion: 1, source: Object.freeze({ identity: data.identity, remote: data.remote,
|
|
184
|
-
selector: data.selector, commit: data.commit, provenance: freezeJson([structuredClone(origin)]) }), accessContextKey });
|
|
185
|
-
handles.set(observation, data);
|
|
186
|
-
return observation;
|
|
187
|
-
},
|
|
188
|
-
readFile(observation, path, { optional = false } = {}) {
|
|
189
|
-
portablePath(path); const data = dataFor(observation), rows = list(data, [path], false);
|
|
190
|
-
if (!rows.length && optional) return null;
|
|
191
|
-
if (rows.length !== 1 || rows[0].path !== path || !["100644", "100755"].includes(rows[0].mode)) throw oatsError("export-not-found", "repository descriptor is absent or is not a regular file");
|
|
192
|
-
const bytes = blob(data, rows[0], 1024 * 1024);
|
|
193
|
-
return { bytes, origin: { kind: "source", source: data.remote, revision: data.commit, path, integrity: bytesIntegrity(bytes) } };
|
|
194
|
-
},
|
|
195
|
-
materialize(observation, roots, destination) {
|
|
196
|
-
const data = dataFor(observation);
|
|
197
|
-
canonicalJson(roots);
|
|
198
|
-
if (!Array.isArray(roots) || !roots.length || roots.length > 1024 || new Set(roots).size !== roots.length) throw oatsError("invalid-source", "source projection needs a bounded unique root set");
|
|
199
|
-
roots.forEach((path) => portablePath(path, { allowRoot: true }));
|
|
200
|
-
if (typeof destination !== "string" || !isAbsolute(destination)) throw oatsError("invalid-source", "source projection destination must be explicit");
|
|
201
|
-
const rows = list(data, roots, true);
|
|
202
|
-
for (const path of roots) if (!rows.some((row) => path === "." || row.path === path || row.path.startsWith(`${path}/`))) throw oatsError("export-not-found", "required source projection root is absent");
|
|
203
|
-
for (const entry of rows) validateProjectionPath(entry.path);
|
|
204
|
-
const expected = new Map(rows.map((entry) => [entry.path, entry]));
|
|
205
|
-
if (expected.size !== rows.length) throw oatsError("invalid-source", "source tree has duplicate paths");
|
|
206
|
-
const projection = createSourceProjection(destination);
|
|
207
|
-
try {
|
|
208
|
-
for (const entry of rows) {
|
|
209
|
-
const bytes = blob(data, entry, maxBytes);
|
|
210
|
-
if (entry.mode === "120000") projection.link(entry.path, decodeUtf8(bytes, "source link target"));
|
|
211
|
-
else if (["100644", "100755"].includes(entry.mode)) projection.file(entry.path, bytes, entry.mode === "100755" ? 0o755 : 0o644);
|
|
212
|
-
else throw oatsError("source-incomplete", "source object mode cannot be retained");
|
|
213
|
-
}
|
|
214
|
-
projection.finish();
|
|
215
|
-
return { directory: destination, roots: [...roots], observation };
|
|
216
|
-
} catch (error) { error.stagingPath = destination; throw error; }
|
|
217
|
-
},
|
|
218
|
-
close() {
|
|
219
|
-
if (closed) return;
|
|
220
|
-
const current = lstatSync(root);
|
|
221
|
-
if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("source-unavailable", "repository transaction cleanup lost ownership");
|
|
222
|
-
closed = true;
|
|
223
|
-
rmSync(root, { recursive: true });
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|