@awebai/oats 0.29.2 → 0.29.4
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/bin/oats.mjs +9 -4
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +19 -18
- package/capabilities/oats-aweb/injects/aweb.md +4 -4
- package/capabilities/oats-aweb/lib/binding-wire.mjs +1 -3
- package/capabilities/oats-aweb/lib/wake-receive.mjs +1 -1
- package/capabilities/oats-aweb/oats.json +3 -3
- package/capabilities/oats-aweb/skills/VENDORED.md +1 -1
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +9 -9
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +1 -1
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +13 -14
- package/capabilities/oats-okf/bin/oats-okf.mjs +9 -6
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +26 -4
- package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -1
- package/capabilities/oats-okf/lib/sources.mjs +18 -1
- package/capabilities/oats-okf/lib/stores.mjs +8 -6
- package/capabilities/oats-okf/lib/worker.mjs +2 -1
- package/capabilities/oats-okf/oats.json +1 -1
- package/capabilities/oats-okf-harvest/oats.json +1 -1
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +35 -14
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
- package/capabilities/oats-okf-maintenance/oats.json +1 -1
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +16 -1
- package/docs/capabilities.md +3 -4
- package/docs/capability-manifest.schema.json +3 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +2 -0
- package/docs/design/2026-09-24-phase-d-plan.md +2 -0
- package/docs/design/2026-09-25-teams-contract.md +36 -4
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +4 -4
- package/docs/design/2026-09-27-team-model-v2.md +136 -0
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +5 -5
- package/docs/release-notes/v0.29.3.md +53 -0
- package/docs/release-notes/v0.29.4.md +90 -0
- package/docs/schedules.md +18 -4
- package/docs/workspaces.md +4 -4
- package/lib/automations.mjs +7 -0
- package/lib/core.mjs +3 -3
- package/lib/packages.mjs +2 -5
- package/lib/resolve.mjs +2 -2
- package/lib/schedule.mjs +31 -17
- package/lib/triggers.mjs +51 -15
- package/lib/workspace.mjs +3 -3
- package/package-catalog.json +3 -3
- package/package.json +1 -1
- package/capabilities/oats-aweb/lib/personal-team.mjs +0 -19
package/bin/oats.mjs
CHANGED
|
@@ -55,7 +55,8 @@ const rawArgs = process.argv.slice(2);
|
|
|
55
55
|
const KERNEL_SWITCHES = new Set(["allow-child-spawns", "apply", "check", "clear", "delete-branch", "discard-worktree", "dry-run", "ephemeral", "force", "help", "host", "json", "keep-dir", "keep-env", "no-child-spawns", "no-launch", "no-recursive", "no-yolo", "plan", "policy", "preview", "print", "replace", "self", "verbose", "yes", "yolo"]);
|
|
56
56
|
/** `--flag=value` is `--flag value`: every kernel reader (flag(), valueFlag(), the onboard and
|
|
57
57
|
* routed-command loops) then applies the spaced form's validation to it. `problem` is an empty
|
|
58
|
-
* `--flag
|
|
58
|
+
* `--flag=`, a switch given a value, or a value that is itself an option (`--model=--yolo`):
|
|
59
|
+
* expanded, it would be a flag token every reader sees, which the spaced form can never carry. */
|
|
59
60
|
function expandInlineValues(argv) {
|
|
60
61
|
const out = [];
|
|
61
62
|
let problem;
|
|
@@ -63,7 +64,7 @@ function expandInlineValues(argv) {
|
|
|
63
64
|
const eq = a.indexOf("=");
|
|
64
65
|
if (!a.startsWith("--") || eq <= 2) { out.push(a); continue; }
|
|
65
66
|
const name = a.slice(2, eq), value = a.slice(eq + 1);
|
|
66
|
-
problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : undefined;
|
|
67
|
+
problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : value.startsWith("--") ? `--${name}= takes a value, not an option (got ${a})` : undefined;
|
|
67
68
|
out.push(`--${name}`, value);
|
|
68
69
|
}
|
|
69
70
|
return { argv: out, problem };
|
|
@@ -2144,7 +2145,7 @@ async function scheduleCmd() {
|
|
|
2144
2145
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
2145
2146
|
const reg = readRegistry();
|
|
2146
2147
|
const tctx = scopeAutomations(ws(), { io, refresh: !dryRun });
|
|
2147
|
-
const considered = withHostLock(() => [...(tctx.refresh && !tctx.refresh.ok ? [{ workspace: ws(), action: "error", error: `automations refresh: ${tctx.refresh.error}` }] : []), ...tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun, ctx: tctx }), ...tickTriggers(ws(), { io, dryRun, ctx: tctx })]);
|
|
2148
|
+
const considered = withHostLock(() => [...(tctx.refresh && !tctx.refresh.ok ? [{ workspace: ws(), action: "error", error: `automations refresh: ${tctx.refresh.error}` }] : []), ...tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun, ctx: tctx }), ...tickTriggers(ws(), { io, dryRun, ctx: tctx, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()] })]);
|
|
2148
2149
|
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
2149
2150
|
}
|
|
2150
2151
|
case "host": {
|
|
@@ -2503,7 +2504,11 @@ async function capabilityCommand() {
|
|
|
2503
2504
|
const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
|
|
2504
2505
|
// No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
|
|
2505
2506
|
// already fetched exactly this commit; otherwise the command gets none (never ambient).
|
|
2506
|
-
|
|
2507
|
+
// The agent directory is the soul entry's own (a package soul's is `<package>--<soul>`), never
|
|
2508
|
+
// the bare name, which a same-named member soul's copy may occupy (as instance-inspect does).
|
|
2509
|
+
const { agentDirOf } = await import("../lib/instance-resolution.mjs");
|
|
2510
|
+
const entry = hit.prepared?.soulEntry;
|
|
2511
|
+
const cachedSoul = entry?.commit ? join(hit.deployment, "agents", agentDirOf(entry), "souls", String(entry.commit).slice(0, 12)) : null;
|
|
2507
2512
|
return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, { settings: hit.settings, origins: hit.resolution?.payloadOrigins?.[hit.module.name] }, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
|
|
2508
2513
|
}
|
|
2509
2514
|
|
|
@@ -45,7 +45,6 @@ import { assessCapturedSessionReadiness, querySelectedKernel } from "../lib/sess
|
|
|
45
45
|
import { runCapturedNative } from "../lib/captured-native.mjs";
|
|
46
46
|
import { CUSTODY_ATTACH_MIN, grantYamlCustodySocket, parseBindingJson } from "../lib/binding-wire.mjs";
|
|
47
47
|
import { custodyPreflight } from "../lib/grant-custody.mjs";
|
|
48
|
-
import { PERSONAL_ROOT_DEFERRED_WARNING, personalRootDeclared } from "../lib/personal-team.mjs";
|
|
49
48
|
import { runtimeDeliveryFor, wakeRegistration } from "../lib/wake-receive.mjs";
|
|
50
49
|
|
|
51
50
|
/** Run a command as ARGV — never a shell string. Team ids, aliases, instance
|
|
@@ -498,7 +497,7 @@ function globalGrantSpawn() {
|
|
|
498
497
|
const teamWarnings = [];
|
|
499
498
|
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
500
499
|
if (!team) team = activeTeamAt(custody);
|
|
501
|
-
if (unmappedPrimary && team) teamWarnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using
|
|
500
|
+
if (unmappedPrimary && team) teamWarnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using the default team ${team}`);
|
|
502
501
|
if (!team) fatal("identity.mode \"global\" requires settings.oats.aweb.team or an active team at the resident custody root before minting a grant");
|
|
503
502
|
const grantHome = join(home, ".aweb-identity");
|
|
504
503
|
if (existsSync(grantHome)) fatal(`${grantHome} already exists; refusing to overwrite an existing aweb session grant home`);
|
|
@@ -758,9 +757,9 @@ function validateJoinLabels(labels, { action = "join" } = {}) {
|
|
|
758
757
|
const eligible = eligibleTeams();
|
|
759
758
|
const byLabel = new Map(eligible.map((t) => [t.label, t]));
|
|
760
759
|
for (const label of labels) {
|
|
761
|
-
if (action === "leave" && label === "
|
|
762
|
-
const error = new Error(`
|
|
763
|
-
error.code = "
|
|
760
|
+
if (action === "leave" && label === "default") {
|
|
761
|
+
const error = new Error(`E_TEAM_DEFAULT: ${label} is the workspace's default team and cannot be left`);
|
|
762
|
+
error.code = "E_TEAM_DEFAULT";
|
|
764
763
|
throw error;
|
|
765
764
|
}
|
|
766
765
|
if (!LABEL_RE.test(label) || !byLabel.has(label)) {
|
|
@@ -801,14 +800,19 @@ function readCapabilityMeta() {
|
|
|
801
800
|
if (process.env.OATS_META) { try { return withProviderTeams(JSON.parse(process.env.OATS_META || "{}")); } catch { return withProviderTeams({}); } }
|
|
802
801
|
try { return withProviderTeams(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")).capabilityMeta?.["oats.aweb"] || {}); } catch { return withProviderTeams({}); }
|
|
803
802
|
}
|
|
803
|
+
function defaultTeamSource(meta = {}) {
|
|
804
|
+
const recorded = meta.defaultTeam?.source;
|
|
805
|
+
if (recorded === "setting" || recorded === "root") return recorded;
|
|
806
|
+
return payloadTeam().team ? "setting" : "root";
|
|
807
|
+
}
|
|
804
808
|
function teamsDocument(meta = readCapabilityMeta()) {
|
|
805
809
|
const teams = parseOatsTeams();
|
|
806
810
|
const joined = joinedTeamsOf(meta);
|
|
807
811
|
const joinedLabels = new Set(joined.map((j) => j.label));
|
|
808
812
|
const primary = primaryTeamLabel();
|
|
809
|
-
const
|
|
813
|
+
const defaultTeam = meta.defaultTeam?.team || meta.team || meta.identity?.team || payloadTeam().team || null;
|
|
810
814
|
return {
|
|
811
|
-
|
|
815
|
+
defaultTeam: { team: defaultTeam, source: defaultTeamSource(meta) },
|
|
812
816
|
primary,
|
|
813
817
|
eligible: teams.filter((t) => t.mapped && t.team).map((t) => ({ label: t.label, team: t.team, joined: joinedLabels.has(t.label) })),
|
|
814
818
|
joined: joined.map((j) => ({ label: j.label, team: j.team, identityHome: j.identityHome, receive: j.receive || "poll", since: j.since })),
|
|
@@ -888,7 +892,7 @@ function parseHomeCommandArgs(argv = process.argv.slice(3)) {
|
|
|
888
892
|
}
|
|
889
893
|
function outputTeamsDocument(doc, json) {
|
|
890
894
|
if (json) { console.log(JSON.stringify(doc)); return; }
|
|
891
|
-
console.log(`
|
|
895
|
+
console.log(`default team: ${doc.defaultTeam.team || "unknown"}`);
|
|
892
896
|
for (const row of doc.eligible) console.log(`${row.joined ? "joined" : "eligible"}: ${row.label} (${row.team})`);
|
|
893
897
|
for (const label of doc.unmapped) console.log(`unmapped: ${label}`);
|
|
894
898
|
}
|
|
@@ -934,9 +938,7 @@ function runTeamsCommand(kind) {
|
|
|
934
938
|
|
|
935
939
|
/** The primary identity's team and the root that mints it, exactly as 1.14.2:
|
|
936
940
|
* the config's `team:` (id, then name) wins, else the root's active team.
|
|
937
|
-
*
|
|
938
|
-
* `aw auth` or `aw team ensure` call and a declared roots.personal is ignored
|
|
939
|
-
* with a warning. Exits through fatal() on refusal. */
|
|
941
|
+
* Exits through fatal() on refusal. */
|
|
940
942
|
function resolvePrimaryTeam() {
|
|
941
943
|
const warnings = [];
|
|
942
944
|
const root = awebRoot();
|
|
@@ -945,7 +947,7 @@ function resolvePrimaryTeam() {
|
|
|
945
947
|
const source = team ? "setting" : "root";
|
|
946
948
|
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
947
949
|
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
948
|
-
if (unmappedPrimary && team) warnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using
|
|
950
|
+
if (unmappedPrimary && team) warnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using the default team ${team}`);
|
|
949
951
|
if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
|
|
950
952
|
// A bare team name (no namespace) resolves against the root's memberships.
|
|
951
953
|
if (!team.includes(":")) {
|
|
@@ -955,7 +957,6 @@ function resolvePrimaryTeam() {
|
|
|
955
957
|
else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — ${teamConfigRemedy()}`);
|
|
956
958
|
else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or ${teamConfigRemedy()}`);
|
|
957
959
|
}
|
|
958
|
-
if (personalRootDeclared(settings)) warnings.push(`oats-aweb: personal-root-deferred — ${PERSONAL_ROOT_DEFERRED_WARNING}`);
|
|
959
960
|
return { team, root, source, warnings };
|
|
960
961
|
}
|
|
961
962
|
|
|
@@ -1110,19 +1111,19 @@ if (event === "launch") {
|
|
|
1110
1111
|
const deliveryBrief = deliveryMode === "session"
|
|
1111
1112
|
? ` Notification delivery: external (AWEB_DELIVERY=session): the host wake broker (aw wake) is registered for this home and nudges you when mail or chat arrives; the native aweb channel is not running. If you have waited long with nothing arriving, check \`aw mail inbox\` and \`aw chat pending\` yourself at task boundaries.`
|
|
1112
1113
|
: "";
|
|
1113
|
-
let meta = { team: joined.team_id, alias, delivery: deliveryMode,
|
|
1114
|
+
let meta = { team: joined.team_id, alias, delivery: deliveryMode, defaultTeam: { team: joined.team_id, source: primary.source }, ...(process.env.OATS_RUNTIME ? { runtime: process.env.OATS_RUNTIME } : {}), identity: identityMeta({ mode: "local", alias, team: joined.team_id }) };
|
|
1114
1115
|
const joinFloorProblem = joinRows.length ? joinedTeamsAwFloorProblem() : undefined;
|
|
1115
1116
|
if (joinFloorProblem) warnings.push(`oats-aweb: ${joinFloorProblem}`);
|
|
1116
1117
|
else for (const row of joinRows) { const result = mintJoinedTeam(row, meta); meta = result.meta; spawnMeta = meta; writeProviderTeamsState(meta); if (result.warning) warnings.push(`oats-aweb: ${result.warning}`); }
|
|
1117
1118
|
if (joinedTeamsOf(meta).length) { const synced = syncWakeReceive(meta); meta = synced.meta; for (const w of synced.warnings) warnings.push(`oats-aweb: ${w}`); }
|
|
1118
1119
|
writeProviderTeamsState(meta);
|
|
1119
|
-
const
|
|
1120
|
+
const defaultTeamBrief = meta.defaultTeam.source === "setting" ? "the team this deployment configured for you" : "the workspace's default team (the messaging root's active team)";
|
|
1120
1121
|
const joinedNow = joinedTeamsOf(meta);
|
|
1121
1122
|
const joinedBrief = joinedNow.length ? ` Joined teams: ${joinedNow.map((j) => `${j.label} (${j.team}, receive ${j.receive}, send with \`aw --identity-home ${j.identityHome} mail|chat ...\`)`).join("; ")}.` : "";
|
|
1122
1123
|
out({
|
|
1123
1124
|
meta,
|
|
1124
1125
|
env,
|
|
1125
|
-
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}, ${
|
|
1126
|
+
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}, ${defaultTeamBrief}.${mismatch}${deliveryBrief}${joinedBrief} Load the oats-aweb skill before messaging: \`oats aweb teams --json\` shows your teams, \`oats aweb roster\` who you can reach. Coordination stays in your deployment's task layer.`,
|
|
1126
1127
|
...(launch ? { launch } : {}),
|
|
1127
1128
|
...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : warnings.length ? { warning: warnings.join(" | ") } : channelWarning ? { warning: channelWarning } : {}),
|
|
1128
1129
|
});
|
|
@@ -1206,7 +1207,7 @@ if (event === "launch") {
|
|
|
1206
1207
|
// alias = instance name, so the team's member roster lists live instances
|
|
1207
1208
|
// wherever they run (plus human members). Local liveness comes from
|
|
1208
1209
|
// `oats status` in the deployment; this is the network view.
|
|
1209
|
-
// Default: this instance's
|
|
1210
|
+
// Default: this instance's default team, listed from the root that minted
|
|
1210
1211
|
// it. `--label <label>` lists an eligible (joined or not) workspace team from
|
|
1211
1212
|
// the host root that holds it.
|
|
1212
1213
|
const argv = process.argv.slice(3);
|
|
@@ -1220,7 +1221,7 @@ if (event === "launch") {
|
|
|
1220
1221
|
team = row.team; root = awebRootForTeam(team);
|
|
1221
1222
|
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate(team))}, so the roster of ${label} cannot be read from this host`); process.exit(1); }
|
|
1222
1223
|
} else {
|
|
1223
|
-
team = meta.
|
|
1224
|
+
team = meta.defaultTeam?.team || meta.identity?.team || meta.team || process.env.OATS_TEAM_ID;
|
|
1224
1225
|
root = awebRoot();
|
|
1225
1226
|
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate())}`); process.exit(1); }
|
|
1226
1227
|
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
## Messaging: aweb
|
|
2
2
|
|
|
3
3
|
Your messaging layer is **aweb**. Your identity's alias is your instance name,
|
|
4
|
-
and it lives in the
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
and it lives in the workspace's default team (the `Comms:` line of your TASK.md
|
|
5
|
+
names it). Wider workspace teams are joined explicitly, each with its own
|
|
6
|
+
identity home.
|
|
7
7
|
|
|
8
8
|
**Load the `oats-aweb` skill before your first `aw mail`/`aw chat` of a session,
|
|
9
9
|
whenever an aweb wake or channel event arrives, and whenever messaging or a
|
|
@@ -14,7 +14,7 @@ memory; if a flag looks wrong run `aw <command> --help`.
|
|
|
14
14
|
Quick crib (run from your instance home, never from `./work`):
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
oats aweb teams --json #
|
|
17
|
+
oats aweb teams --json # defaultTeam, eligible, joined teams
|
|
18
18
|
oats aweb roster # who you can reach
|
|
19
19
|
aw mail inbox # UNREAD mail only (--show-all: history)
|
|
20
20
|
aw mail send --to <alias> --subject "..." --body-file <f> # recipient needs --to
|
|
@@ -4,7 +4,6 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import { TextDecoder } from 'node:util';
|
|
5
5
|
import { assessCapturedSessionReadiness } from './session-readiness.mjs';
|
|
6
6
|
import { custodyPreflight } from './grant-custody.mjs';
|
|
7
|
-
import { PERSONAL_ROOT_DEFERRED_WARNING, personalRootDeclared } from './personal-team.mjs';
|
|
8
7
|
import { joinedReceiveModes } from './wake-receive.mjs';
|
|
9
8
|
import {
|
|
10
9
|
MESSAGING_CONTRACT,
|
|
@@ -220,9 +219,8 @@ function readinessDetails(settings,{deployment,env=process.env}={}) {
|
|
|
220
219
|
const initialTeam=typeof settings.team==='string' && settings.team.trim()?settings.team.trim():undefined;
|
|
221
220
|
const candidate=rootCandidate(settings,initialTeam,{deployment,env}),team=teamFromSettings(settings,candidate,{env}),problems=[],warnings=[];
|
|
222
221
|
if(!candidate.root || !isAbsolute(candidate.root) || !existsSync(join(resolve(candidate.root),'.aw'))) problems.push({code:'needs-configuration',message:`no messaging root at ${candidate.root?resolve(candidate.root):process.cwd()}: run oats aweb setup there or set ${candidate.key}`});
|
|
223
|
-
const unmapped=unmappedPrimary(env);if(unmapped&&team)warnings.push({code:'team-unmapped',message:`workspace label ${unmapped.label} is not mapped; using
|
|
222
|
+
const unmapped=unmappedPrimary(env);if(unmapped&&team)warnings.push({code:'team-unmapped',message:`workspace label ${unmapped.label} is not mapped; using the default team ${team}`});
|
|
224
223
|
if(!team) problems.push({code:'needs-configuration',message:'no team: set settings.oats.aweb.team or keep an active team at the aweb root'});
|
|
225
|
-
if(personalRootDeclared(settings)) warnings.push({code:'personal-root-deferred',message:PERSONAL_ROOT_DEFERRED_WARNING});
|
|
226
224
|
return {team,candidate,warnings,result:checkProblems(problems) || {status:'ready',problems:[]}};
|
|
227
225
|
}
|
|
228
226
|
function readinessFromSettings(settings,options) {return readinessDetails(settings,options).result;}
|
|
@@ -30,7 +30,7 @@ export function wakeRegistration({home, primaryIdentityHome, delivery, runtime,
|
|
|
30
30
|
if (!rt || !joined.length) return null;
|
|
31
31
|
const receive = joined.map((j) => ({identity_home: j.identityHome, label: j.label, event_classes: JOINED_EVENT_CLASSES}));
|
|
32
32
|
const doc = rt === 'external-session'
|
|
33
|
-
? {home, delivery: 'session', runtime_delivery: rt, identity_home: primaryIdentityHome, receive_identities: [{identity_home: primaryIdentityHome, label: '
|
|
33
|
+
? {home, delivery: 'session', runtime_delivery: rt, identity_home: primaryIdentityHome, receive_identities: [{identity_home: primaryIdentityHome, label: 'default', controls: true}, ...receive]}
|
|
34
34
|
: {home, delivery: rt, runtime_delivery: rt, primary_identity_home: primaryIdentityHome, receive_identities: receive};
|
|
35
35
|
if (backend) doc.backend = backend;
|
|
36
36
|
return doc;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.aweb",
|
|
3
3
|
"command": "aweb",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.16.0",
|
|
5
5
|
"compatibility": {
|
|
6
6
|
"oats": ">=0.26.0"
|
|
7
7
|
},
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"description": "channel: the native aweb channel packages wake the instance (default). session: delivery is external (AWEB_DELIVERY=session), no channel flag; the host wake broker registers the instance once it exists."
|
|
143
143
|
},
|
|
144
144
|
"team": {
|
|
145
|
-
"description": "Target aweb team id for the primary
|
|
145
|
+
"description": "Target aweb team id for the primary identity lifecycle. In workspace v2 spawns this payload value wins over OATS_TEAM_ID; if both are set and differ the hook warns and uses this setting. Unset means the aweb root's active team."
|
|
146
146
|
},
|
|
147
147
|
"root": {
|
|
148
148
|
"hostOnly": true,
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
},
|
|
151
151
|
"roots": {
|
|
152
152
|
"hostOnly": true,
|
|
153
|
-
"description": "Host-owned map of aweb team id to absolute directory whose .aw is that team's minting root, for deployments that mint into several teams. Put this only in oats-local.yaml settings.oats.aweb.roots.
|
|
153
|
+
"description": "Host-owned map of aweb team id to absolute directory whose .aw is that team's minting root, for deployments that mint into several teams. Put this only in oats-local.yaml settings.oats.aweb.roots. Keys are team ids only."
|
|
154
154
|
},
|
|
155
155
|
"identity": {
|
|
156
156
|
"default": {
|
|
@@ -16,7 +16,7 @@ Vendored trees:
|
|
|
16
16
|
- `aweb-identity/`
|
|
17
17
|
|
|
18
18
|
Not vendored: `oats-aweb/` is this package's own OATS playbook (identity,
|
|
19
|
-
|
|
19
|
+
default and joined teams, roster, delivery and wakes, etiquette,
|
|
20
20
|
troubleshooting). Every `aw` invocation it and the vendored skills cite is
|
|
21
21
|
checked against a real published aw by `test/oats-aweb-1-15.test.mjs`.
|
|
22
22
|
|
|
@@ -23,12 +23,12 @@ retire cleanup, readiness, and Desktop operations consistent.
|
|
|
23
23
|
Use the provider commands from the instance home (or with `--home <path>`):
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
oats aweb teams --json #
|
|
26
|
+
oats aweb teams --json # defaultTeam, eligible, joined, unmapped
|
|
27
27
|
oats aweb join --labels <label>[,<label>] # join eligible workspace labels
|
|
28
28
|
oats aweb leave --labels <label>[,<label>] # leave joined wider-team labels
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
- The
|
|
31
|
+
- The workspace's default team cannot be left; attempting it with label `default` is `E_TEAM_DEFAULT`.
|
|
32
32
|
- A label that is not eligible for this soul/workspace is `E_TEAM_NOT_ELIGIBLE`.
|
|
33
33
|
- Joined wider teams use a local identity home such as
|
|
34
34
|
`<home>/.aweb-identity-<label>`. Joined teams require aw >= 1.36.12. The
|
|
@@ -62,15 +62,15 @@ aw id cert show
|
|
|
62
62
|
|
|
63
63
|
Interpret common states:
|
|
64
64
|
|
|
65
|
-
- `teams.
|
|
66
|
-
the aweb root's active team (`
|
|
67
|
-
team (`setting`).
|
|
65
|
+
- `teams.defaultTeam.team` is the primary identity's team, wired to the harness:
|
|
66
|
+
the aweb root's active team (`defaultTeam.source: root`) or a deployment-pinned
|
|
67
|
+
team (`setting`).
|
|
68
68
|
- `eligible[]` are labels this soul/workspace may explicitly join; the primary
|
|
69
69
|
label may appear here and is joinable/leavable like any other wider team.
|
|
70
70
|
- `joined[]` are provider-created wider-team memberships; each has an
|
|
71
71
|
`identityHome`, `since`, and `receive` (`native` or `poll`).
|
|
72
72
|
- `unmapped[]` labels are present on the soul but not mapped by the workspace.
|
|
73
|
-
An unmapped primary falls back to the
|
|
73
|
+
An unmapped primary falls back to the default/root active team with a
|
|
74
74
|
`team-unmapped` warning; it is not a spawn blocker.
|
|
75
75
|
- `teams-unverified` on launch means the kernel supplied recorded/unknown team
|
|
76
76
|
data, so the provider kept memberships instead of leaving anything.
|
|
@@ -81,9 +81,9 @@ Interpret common states:
|
|
|
81
81
|
`default:oats.aweb.ai`).
|
|
82
82
|
- **Team certificate**: a signed membership statement for an identity; stored in
|
|
83
83
|
`.aw/team-certs/` for native identities.
|
|
84
|
-
- **
|
|
85
|
-
aweb root's active team, or `settings.oats.aweb.team` when the deployment
|
|
86
|
-
pins one.
|
|
84
|
+
- **Default team**: the workspace default team for the instance's primary identity:
|
|
85
|
+
the aweb root's active team, or `settings.oats.aweb.team` when the deployment
|
|
86
|
+
pins one.
|
|
87
87
|
- **Joined team**: an explicit wider team joined through `oats aweb join`, with a
|
|
88
88
|
separate local identity home.
|
|
89
89
|
|
package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md
CHANGED
|
@@ -13,7 +13,7 @@ These layers can combine in multiple ways. Do not assume one from another. The c
|
|
|
13
13
|
|
|
14
14
|
Fully Hosted means aweb operates namespace and team authority for hosted domains such as `*.aweb.ai`. It can mint hosted team certificates and provide simple onboarding. This is the simple default for most users.
|
|
15
15
|
|
|
16
|
-
Hosted OAuth/MCP flows provision custodial addressed/global identities,
|
|
16
|
+
Hosted OAuth/MCP flows provision custodial addressed/global identities, default team membership, and harness credentials before a local CLI workspace exists. Team API-key CLI bootstrap is different: it creates a local self-custodial CLI workspace in a hosted team. In OAuth/MCP flows, use CLI checks for diagnosis only when a local workspace is actually involved; do not force BYOT setup.
|
|
17
17
|
|
|
18
18
|
## BYOT
|
|
19
19
|
|
|
@@ -23,16 +23,16 @@ joined team, put `--identity-home <identityHome>` before the subcommand.
|
|
|
23
23
|
| Fact | Where to read it |
|
|
24
24
|
|---|---|
|
|
25
25
|
| Your alias | your instance name; the `Comms:` line of `TASK.md`; `aw whoami` |
|
|
26
|
-
| Your
|
|
26
|
+
| Your default team | `oats aweb teams --json` → `defaultTeam.team` (`defaultTeam.source`) |
|
|
27
27
|
| Teams you may join | `oats aweb teams --json` → `eligible[]` |
|
|
28
28
|
| Teams you have joined | `oats aweb teams --json` → `joined[]` (each with `identityHome`, `receive`) |
|
|
29
29
|
| How mail reaches you | the `Comms:` line of `TASK.md` (see section 4) |
|
|
30
30
|
|
|
31
|
-
- **
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
- **Default team.** Your primary identity lives in the workspace's default team:
|
|
32
|
+
the aweb root's active team (`defaultTeam.source: root`), or the team the
|
|
33
|
+
deployment pinned (`defaultTeam.source: setting`). `defaultTeam.source` is
|
|
34
|
+
always present and is only `root` or `setting`. Everyone this deployment
|
|
35
|
+
spawns into that team is there with you.
|
|
36
36
|
- **Joined teams.** A wider team the workspace defines, joined explicitly. Each
|
|
37
37
|
gives you a **separate identity** with the same alias in that team, kept
|
|
38
38
|
under `<home>/.aweb-identity-<label>`. You act as that team only with
|
|
@@ -42,7 +42,7 @@ joined team, put `--identity-home <identityHome>` before the subcommand.
|
|
|
42
42
|
## 2. Find who to talk to
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
oats aweb roster # your
|
|
45
|
+
oats aweb roster # your default team's members (instances + humans), across machines
|
|
46
46
|
oats aweb roster --label <label> # an eligible workspace team's members
|
|
47
47
|
oats status # live OATS instances on this machine
|
|
48
48
|
```
|
|
@@ -52,7 +52,7 @@ oats status # live OATS instances on this machine
|
|
|
52
52
|
- Outside your team use a full address, `namespace/alias` (`--to-address`), only
|
|
53
53
|
when you were given one.
|
|
54
54
|
- A name that is not on the roster of the team you send from will not resolve:
|
|
55
|
-
pick the identity (
|
|
55
|
+
pick the identity (default-team or joined) whose team holds the recipient.
|
|
56
56
|
|
|
57
57
|
## 3. Send, reply, chat
|
|
58
58
|
|
|
@@ -126,7 +126,7 @@ mail (`--show-all`).
|
|
|
126
126
|
## 5. Teams: join and leave
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
oats aweb teams --json # {
|
|
129
|
+
oats aweb teams --json # {defaultTeam, primary, eligible, joined, unmapped}
|
|
130
130
|
oats aweb join --labels <label>[,<label>]
|
|
131
131
|
oats aweb leave --labels <label>[,<label>]
|
|
132
132
|
```
|
|
@@ -134,7 +134,7 @@ oats aweb leave --labels <label>[,<label>]
|
|
|
134
134
|
- Join only when your human, coordinator or task asks you to work with that
|
|
135
135
|
team. Joining mints a new identity for you in that team.
|
|
136
136
|
- You may join only `eligible[]` labels; anything else is `E_TEAM_NOT_ELIGIBLE`.
|
|
137
|
-
-
|
|
137
|
+
- The workspace's default team cannot be left (`E_TEAM_DEFAULT` when the label is `default`).
|
|
138
138
|
- When the workspace stops mapping a team, your next session start leaves it.
|
|
139
139
|
- Do not run native `aw team join|switch|leave|invite` for your identities; the
|
|
140
140
|
provider keeps homes, broker registration and retire cleanup consistent.
|
|
@@ -165,7 +165,7 @@ Check your own state first:
|
|
|
165
165
|
```bash
|
|
166
166
|
aw whoami # identity you act as here
|
|
167
167
|
aw workspace status # connection of the primary identity
|
|
168
|
-
oats aweb teams --json #
|
|
168
|
+
oats aweb teams --json # defaultTeam/joined teams and receive modes
|
|
169
169
|
oats readiness --home "$PWD" --json # the provider's readiness answer for this home
|
|
170
170
|
```
|
|
171
171
|
|
|
@@ -173,8 +173,7 @@ oats readiness --home "$PWD" --json # the provider's readiness answer for this
|
|
|
173
173
|
|
|
174
174
|
| Code | Meaning | Who fixes it |
|
|
175
175
|
|---|---|---|
|
|
176
|
-
| `
|
|
177
|
-
| `team-unmapped` | your soul's primary label is not mapped by the workspace; you are in the personal team | workspace owner, if a shared team was meant |
|
|
176
|
+
| `team-unmapped` | your soul's primary label is not mapped by the workspace; you are in the default team | workspace owner, if a shared team was meant |
|
|
178
177
|
| `joined-team-receive` | a joined team receives live through the broker (informational) | nobody |
|
|
179
178
|
| `joined-team-poll-only` | a joined team does not wake you; the message says why | poll that team at task boundaries; human may start the wake daemon |
|
|
180
179
|
| `wake-daemon-not-running` / `-outdated` / `-version-unknown` | host wake broker is down or older than 1.36.5 | human: upgrade aw, restart the host wake daemon |
|
|
@@ -185,7 +184,7 @@ oats readiness --home "$PWD" --json # the provider's readiness answer for this
|
|
|
185
184
|
|
|
186
185
|
- `E_TEAM_NOT_ELIGIBLE` — the label is not one of your eligible teams; the
|
|
187
186
|
message lists them. Check the spelling against `oats aweb teams --json`.
|
|
188
|
-
- `
|
|
187
|
+
- `E_TEAM_DEFAULT` — the workspace's default team cannot be left.
|
|
189
188
|
- `E_TEAM_GLOBAL_MODE` — this home acts as a resident identity through a
|
|
190
189
|
session grant; joined teams need local identities. Report it.
|
|
191
190
|
- `E_TEAM_AW_FLOOR` — the host `aw` is too old: joined teams need aw >= 1.36.12.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { fs, join, resolve, readJSON, safePath, oats, fail, unlock } from '../lib/io.mjs';
|
|
2
|
+
import { fs, join, resolve, readJSON, safePath, oats, fail, unlock, redactUrls } from '../lib/io.mjs';
|
|
3
3
|
import { loadBindings } from '../lib/config.mjs';
|
|
4
|
-
import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath, harvestOffRecord } from '../lib/sources.mjs';
|
|
4
|
+
import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath, harvestOffRecord, sourceSwitch, retireHarvestOff } from '../lib/sources.mjs';
|
|
5
5
|
import { harvestStatus, setupHarvest } from '../lib/harvest-status.mjs';
|
|
6
6
|
import { settings } from '../lib/config.mjs';
|
|
7
7
|
import { CONSULT } from '../lib/consult.mjs';
|
|
@@ -151,7 +151,10 @@ else {
|
|
|
151
151
|
if(['STATE.md','log.md','notes','.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p)))) fail('E_MIGRATION','unregistered/legacy source has memory; explicitly migrate/register before retirement');
|
|
152
152
|
result={meta:{retired:true,reason:'nothing-to-delete'}};
|
|
153
153
|
} else {
|
|
154
|
-
const s=src()
|
|
154
|
+
const s=src(),sw=sourceSwitch(s);
|
|
155
|
+
// okf 4.0.1 #6: harvest switched off since spawn (deployment or soul) → no final capture.
|
|
156
|
+
if(sw.effective!=='on') result={meta:retireHarvestOff(s,sw),brief:`Harvest is now off (${sw.reason}): no final capture was taken; earlier inputs stay in custody.`};
|
|
157
|
+
else {scheduleSource(s);const r=capture(s,{final:true});const schedule=settleRetiredSchedule(s);result={meta:{retired:r.complete===true,source:s.file,capture:r,schedule},brief:'Final input is in durable custody. Delivery remains asynchronous.'};}
|
|
155
158
|
}
|
|
156
159
|
} else if(event==='harvest') {
|
|
157
160
|
if(capturedHarvest) {
|
|
@@ -169,8 +172,8 @@ else {
|
|
|
169
172
|
// The deployment can switch harvest off after a source registered: the
|
|
170
173
|
// job then captures and processes nothing (the soul's opt-out was already
|
|
171
174
|
// applied at registration). Nothing drains when it is switched back on.
|
|
172
|
-
const source=src();
|
|
173
|
-
result=
|
|
175
|
+
const source=src(),sw=sourceSwitch(source);
|
|
176
|
+
result=sw.effective!=='on'?{status:'harvest-off',source:source.file,reason:`${sw.reason}; nothing was captured`}
|
|
174
177
|
:runSource(source,{manual:!!flags.manual,noLaunch:!!flags['no-launch']});
|
|
175
178
|
}
|
|
176
179
|
else if(event==='complete') {const s=src();if(captured) retainedRun(s);result=complete(s,flags.run,flags.judgment && resolve(flags.judgment));}
|
|
@@ -196,7 +199,7 @@ else {
|
|
|
196
199
|
} else if(event==='unlock') result=unlock(resolve(flags.lock),flags.token);
|
|
197
200
|
else fail('E_USAGE',`unknown command ${event}; see --help`);
|
|
198
201
|
answer=hook?result:{schemaVersion:1,ok:true,result};
|
|
199
|
-
} catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:
|
|
202
|
+
} catch(e) {const code=e.code || 'E_OKF',message=redactUrls(e.message);exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:message}:{})},warning:`oats-okf ${code}: ${message}`}:{schemaVersion:1,ok:false,error:{code,message}};}
|
|
200
203
|
// Consult commands print text unless --json; every other answer is JSON.
|
|
201
204
|
// Let Node drain the pipe; no process.exit after a possibly large answer.
|
|
202
205
|
if(textMode && exit) process.stderr.write(`oats okf ${event}: ${answer.error.code}: ${answer.error.message}\n`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsolute } from 'node:path';
|
|
2
|
-
import { fs, join, resolve, dirname, fail, readJSON, safePath, relPath, identifier, overlaps, hash } from './io.mjs';
|
|
2
|
+
import { fs, join, resolve, dirname, fail, readJSON, safePath, relPath, identifier, overlaps, hash, embedsCredential } from './io.mjs';
|
|
3
3
|
const obj = v => v && typeof v === 'object' && !Array.isArray(v);
|
|
4
4
|
function keys(value, allowed, label, code='E_CONFIG') {
|
|
5
5
|
if(!obj(value)) fail(code, `${label} must be an object`);
|
|
@@ -48,6 +48,7 @@ export function validateBindings(doc, file, { sourceHome, sourceWork } = {}) {
|
|
|
48
48
|
keys(raw,['id','kind','repository','root','acceptedBranch','pr'],'git base');
|
|
49
49
|
keys(raw.pr,['repository'],'git pr');
|
|
50
50
|
if(typeof raw.repository !== 'string' || !raw.repository || raw.repository.startsWith('-') || /[\r\n\0]/.test(raw.repository) || 'path' in raw) fail('E_CONFIG', 'git base requires repository');
|
|
51
|
+
if(embedsCredential(raw.repository)) fail('E_CONFIG', `git base "${alias}": the repository locator embeds a credential (user:token@); bind the plain URL and let a Git credential helper or SSH key supply access`);
|
|
51
52
|
const root = relPath(raw.root, true);
|
|
52
53
|
if (typeof raw.acceptedBranch !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(raw.acceptedBranch) || raw.acceptedBranch.includes('..') || raw.acceptedBranch.endsWith('/') || raw.acceptedBranch.endsWith('.lock')) fail('E_CONFIG','invalid acceptedBranch');
|
|
53
54
|
if (!obj(raw.pr) || typeof raw.pr.repository !== 'string' || !/^[\w.-]+\/[\w.-]+$/.test(raw.pr.repository)) fail('E_CONFIG','git pr requires repository: owner/repo (same-repository PRs)');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { hostname } from 'node:os';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { fs, join, dirname, safePath, readJSON, save, digest, tree, withLock, fail, syncDir, identifier } from './io.mjs';
|
|
8
|
+
import { fs, join, dirname, resolve, safePath, readJSON, save, digest, tree, withLock, fail, syncDir, identifier, relPath, within, displayRepo } from './io.mjs';
|
|
9
9
|
import { noGit, gitTimeoutMs, consultMaxAgeMs, splitRef, resolveNodes } from './config.mjs';
|
|
10
10
|
import { git, gitEnv, validateBase, baseLock, journalPath, preflightLocalRepository, requireNotShallow, verifyRemote, unavailable } from './stores.mjs';
|
|
11
11
|
|
|
@@ -248,6 +248,23 @@ export function withBase(bindings, alias, { fresh = false } = {}, fn) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// ---------------------------------------------------------------- validation
|
|
251
|
+
/** A tree entry name, from an untrusted accepted tree, as a path under the base
|
|
252
|
+
* root. Git accepts literal `..`, absolute-looking and control-character entry
|
|
253
|
+
* names (`hash-object --literally`), and they survive a partial clone, so every
|
|
254
|
+
* name gets relPath's canonical rules (no empty, `.`, `..`, absolute, backslash,
|
|
255
|
+
* NUL or `.git` segment) plus no control characters or percent-escapes. */
|
|
256
|
+
export function treeEntryPath(name) {
|
|
257
|
+
const bad = () => fail('E_PATH', `invalid tree entry in knowledge base: ${JSON.stringify(String(name)).slice(0, 200)}`);
|
|
258
|
+
if (typeof name !== 'string' || /[\u0000-\u001f\u007f]/.test(name) || /%[0-9a-fA-F]{2}/.test(name)) bad();
|
|
259
|
+
try { return relPath(name); } catch { return bad(); }
|
|
260
|
+
}
|
|
261
|
+
/** Where a tree entry lands under `root`, refused unless it stays strictly
|
|
262
|
+
* inside it after resolution (the second, independent guard). */
|
|
263
|
+
export function containedTarget(root, name) {
|
|
264
|
+
const target = resolve(root, treeEntryPath(name));
|
|
265
|
+
if (target === resolve(root) || !within(root, target)) fail('E_PATH', `tree entry escapes its materialization root: ${JSON.stringify(name).slice(0, 200)}`);
|
|
266
|
+
return safePath(target);
|
|
267
|
+
}
|
|
251
268
|
const VALIDATION_CODES = new Set(['E_VALIDATION', 'E_BASE', 'E_PATH', 'E_ID', 'E_CONFIG']);
|
|
252
269
|
/** Whether the accepted state is a valid OKF base. Git verdicts are cached per
|
|
253
270
|
* commit inside the host cache; computing one materializes the base root into
|
|
@@ -264,10 +281,15 @@ export function verdict(ctx) {
|
|
|
264
281
|
try {
|
|
265
282
|
const result = judge(() => {
|
|
266
283
|
const rows = lsTree(ctx, '', { recursive: true });
|
|
267
|
-
|
|
284
|
+
// Every entry is judged before anything is written: the first bad name or
|
|
285
|
+
// mode refuses the whole base, and nothing lands outside the scratch.
|
|
286
|
+
for (const r of rows) {
|
|
287
|
+
if (!['100644', '100755'].includes(r.mode) || r.type !== 'blob') fail('E_PATH', 'symlink or submodule in knowledge base is not allowed');
|
|
288
|
+
r.target = containedTarget(scratch, r.name);
|
|
289
|
+
}
|
|
268
290
|
prefetch(ctx, '');
|
|
269
291
|
for (const r of rows) {
|
|
270
|
-
const target =
|
|
292
|
+
const target = r.target; fs.mkdirSync(dirname(target), { recursive: true });
|
|
271
293
|
const fd = fs.openSync(target, 'wx', 0o600);
|
|
272
294
|
let w; try { w = spawnSync('git', ['--no-replace-objects', '-c', 'core.hooksPath=/dev/null', '-C', ctx.cache, 'cat-file', 'blob', r.oid], { env: gitEnv(), timeout: gitTimeoutMs(), stdio: ['ignore', fd, 'pipe'] }); } finally { fs.closeSync(fd); }
|
|
273
295
|
if (w.error || w.status !== 0) unavailable(ctx.base, ctx.alias, 'read', gitError(w));
|
|
@@ -346,7 +368,7 @@ export function bases(source, flags) {
|
|
|
346
368
|
const rows = [], receipts = [];
|
|
347
369
|
for (const [alias, base] of Object.entries(source.bindings.bases)) withBase(source.bindings, alias, { fresh: !!flags.fresh }, ctx => {
|
|
348
370
|
const v = verdict(ctx), mine = relation => source.decl[relation].filter(ref => splitRef(ref)[0] === alias).map(ref => splitRef(ref)[1]);
|
|
349
|
-
rows.push({ alias, id: base.id, kind: base.kind, ...(base.kind === 'git' ? { repository: base.repository, acceptedBranch: base.acceptedBranch, root: base.root, commit: ctx.commit } : { path: base.path, digest: ctx.receipt.digest }),
|
|
371
|
+
rows.push({ alias, id: base.id, kind: base.kind, ...(base.kind === 'git' ? { repository: displayRepo(base.repository), acceptedBranch: base.acceptedBranch, root: base.root, commit: ctx.commit } : { path: base.path, digest: ctx.receipt.digest }),
|
|
350
372
|
fetchedAt: ctx.receipt.fetchedAt, stale: ctx.receipt.stale, ...(ctx.receipt.reason ? { reason: ctx.receipt.reason } : {}),
|
|
351
373
|
validated: v.ok ? { ok: true } : { ok: false, error: v.error }, nodes: v.ok ? Object.keys(v.nodes) : null, owns: mine('owns'), reads: mine('reads'), receipt: ctx.receipt });
|
|
352
374
|
receipts.push(ctx.receipt);
|
|
@@ -60,11 +60,24 @@ function shaped(value) {
|
|
|
60
60
|
* A soul `on` is ignored and reported: a soul cannot switch a host on. It also
|
|
61
61
|
* hides the host's value (the kernel's merged `harvest` is then the soul's),
|
|
62
62
|
* so with a soul `on` the switch stays off until the soul drops the line. */
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/** OATS_SETTINGS_ORIGINS (kernel 0.29.0): JSON pointer → { kind, at } of the LAST
|
|
64
|
+
* layer that set each leaf. {} when absent or unreadable. */
|
|
65
|
+
export function parseOrigins(text = process.env.OATS_SETTINGS_ORIGINS) {
|
|
66
|
+
try { const o = JSON.parse(text || '{}'); return o && typeof o === 'object' && !Array.isArray(o) ? o : {}; } catch { return {}; }
|
|
67
|
+
}
|
|
68
|
+
/** okf 4.0.1: origins name the last layer only, and the host's settings merge
|
|
69
|
+
* AFTER the soul's slot, so a host `harvest: on` hides a soul's `off` there.
|
|
70
|
+
* soul.yaml therefore stays the authority for the absolute opt-out; origins
|
|
71
|
+
* are an extra signal: a merged value whose origin is the soul is never the
|
|
72
|
+
* deployment switching harvest on (a soul `on` is ignored, a soul `off` is off). */
|
|
73
|
+
export function harvestSwitch({ settings = {}, soulDir = process.env.OATS_SOUL, origins = parseOrigins() } = {}) {
|
|
74
|
+
const origin = origins?.['/harvest'] && typeof origins['/harvest'] === 'object' ? origins['/harvest'] : null;
|
|
75
|
+
const fromSoul = origin?.kind === 'soul';
|
|
76
|
+
const deployment = !fromSoul && VALUES.includes(settings.harvest) ? settings.harvest : 'off';
|
|
65
77
|
const soul = soulHarvest(soulDir);
|
|
78
|
+
if (fromSoul && VALUES.includes(settings.harvest) && soul.value === null) Object.assign(soul, { value: settings.harvest, readable: true, why: `OATS_SETTINGS_ORIGINS: harvest: ${settings.harvest} came from the soul (${String(origin.at || 'soul.yaml')})` });
|
|
66
79
|
const rows = [
|
|
67
|
-
{ layer: 'deployment', value: deployment, why: settings.harvest === undefined ? 'harvest is not set (default off)' : `settings.oats.okf.harvest: ${settings.harvest}` },
|
|
80
|
+
{ layer: 'deployment', value: deployment, ...(origin ? { origin: origin.kind } : {}), why: fromSoul ? 'the merged harvest value came from the soul, so the deployment does not switch harvest on' : settings.harvest === undefined ? 'harvest is not set (default off)' : `settings.oats.okf.harvest: ${settings.harvest}` },
|
|
68
81
|
{ layer: 'soul', value: soul.value, readable: soul.readable, why: soul.why },
|
|
69
82
|
];
|
|
70
83
|
const warnings = [];
|
|
@@ -8,6 +8,14 @@ export const fail = (code, message) => { throw Object.assign(new Error(message),
|
|
|
8
8
|
export const hash = value => createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : JSON.stringify(value)).digest('hex');
|
|
9
9
|
export const readJSON = path => JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
10
10
|
export const within = (root, path) => { const r = relative(root, path); return r === '' || (!r.startsWith('..' + sep) && r !== '..' && !isAbsolute(r)); };
|
|
11
|
+
// okf 4.0.1 #2: a URL's userinfo is a credential (https://user:token@host).
|
|
12
|
+
// Strip it from anything shown to agents or users; an SSH user alone
|
|
13
|
+
// (ssh://git@host) is not a secret and stays.
|
|
14
|
+
const USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@'"]+)@/gi;
|
|
15
|
+
export const redactUrls = text => String(text).replace(USERINFO, (m, scheme, info) => /^ssh:/i.test(scheme) && !info.includes(':') ? m : scheme);
|
|
16
|
+
export const displayRepo = repository => typeof repository === 'string' ? redactUrls(repository) : repository;
|
|
17
|
+
/** Whether a repository locator embeds a credential (any userinfo except a bare SSH user). */
|
|
18
|
+
export const embedsCredential = repository => { USERINFO.lastIndex = 0; return typeof repository === 'string' && redactUrls(repository) !== repository; };
|
|
11
19
|
export const overlaps = (a, b) => within(a, b) || within(b, a);
|
|
12
20
|
export function safePath(path) {
|
|
13
21
|
path = resolve(path);
|
|
@@ -77,7 +85,7 @@ export function unlock(path, token) {
|
|
|
77
85
|
try { process.kill(o.pid, 0); fail('E_LOCKED', 'lock owner is still alive'); } catch (e) { if(e.code !== 'ESRCH') throw e; }
|
|
78
86
|
fs.rmSync(path, { recursive: true }); syncDir(dirname(path)); return { unlocked: path };
|
|
79
87
|
}
|
|
80
|
-
export const identityKeys = ['OATS_INSTANCE','OATS_INSTANCE_HOME','OATS_HOME','PI_AGENT_HOME','PI_AGENT_NAME','PI_AGENT_INSTANCE','PI_AGENTS_ROOT','OATS_ROOT','OATS_SOUL','OATS_SOUL_ID','OATS_AGENT','OATS_KIND','OATS_EVENT','OATS_CONTEXT','OATS_REPO','OATS_WORK','OATS_BRANCH','OATS_META','OATS_SETTINGS','OATS_BINDING_FILE','OATS_SOURCE_RECEIPT_FILE','OATS_INVOCATION_CONTEXT_FILE','OATS_DEPLOYMENT','OATS_RESOLUTION'];
|
|
88
|
+
export const identityKeys = ['OATS_INSTANCE','OATS_INSTANCE_HOME','OATS_HOME','PI_AGENT_HOME','PI_AGENT_NAME','PI_AGENT_INSTANCE','PI_AGENTS_ROOT','OATS_ROOT','OATS_SOUL','OATS_SOUL_ID','OATS_AGENT','OATS_KIND','OATS_EVENT','OATS_CONTEXT','OATS_REPO','OATS_WORK','OATS_BRANCH','OATS_META','OATS_SETTINGS','OATS_SETTINGS_ORIGINS','OATS_BINDING_FILE','OATS_SOURCE_RECEIPT_FILE','OATS_INVOCATION_CONTEXT_FILE','OATS_DEPLOYMENT','OATS_RESOLUTION'];
|
|
81
89
|
export function cleanEnv(env = process.env) {
|
|
82
90
|
return Object.fromEntries(Object.entries(env).filter(([k]) => !/^(OATS_(?!HOME_DIR$|PACKAGE_CATALOG$)|PI_AGENT|GIT_)/.test(k)));
|
|
83
91
|
}
|