omp-conductor 0.4.4 → 0.5.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.
@@ -17,25 +17,24 @@ inside your own worktree.
17
17
  - **Your branch:** `{{BRANCH}}` — already created for you off the repo's default
18
18
  branch. Never switch branches and never touch a path outside the worktree (write/edit/read/grep/glob are also blocked mechanically outside this checkout; `bash` is still a must-not — do not use it to escape).
19
19
 
20
- **You are running without GitHub credentials.** That is deliberate, not a
21
- misconfiguration (#125): this session is a separate OS principal from the
22
- dispatcher, and the dispatcher performs every network operation on your behalf.
23
- So `gh auth status` failing, `git push` failing, and `~/.ssh` being unreadable
24
- are all the system working. Do not try to work around any of them there is
25
- nothing to find, and the turns are better spent on the issue.
20
+ **You have this host's GitHub credentials, and you must not publish with them.**
21
+ Every push and every PR goes through the four `conductor_*` tools below, because
22
+ the dispatcher owns the settlement record: work it did not perform is work it
23
+ cannot account for, and the checks that would have refused a bad push never ran.
24
+ Reading with `gh` is fine; `git push`, `gh pr create` and `gh pr merge` are not
25
+ yours to run.
26
26
 
27
27
  Read the issue first. **This brief is your copy of it**: the acceptance criteria
28
28
  below were rendered from the issue at dispatch, so you already have what you
29
- need. If a read-scoped token has been configured for this fleet, `gh` reads also
30
- work and are worth one call for the discussion the brief did not copy down:
29
+ need. One `gh` read is still worth the call, for the discussion the brief did
30
+ not copy down:
31
31
 
32
32
  ```bash
33
33
  gh issue view {{ISSUE_NUMBER}} --repo {{TRACKER_REPO}} --comments
34
34
  ```
35
35
 
36
- If that command reports you are not logged in, that is the expected default.
37
- Work from this brief and say so in your report rather than treating it as a
38
- blocker.
36
+ If that command fails, work from this brief and say so in your report rather
37
+ than treating it as a blocker.
39
38
 
40
39
  Then read the repo's own guidance before writing anything: `AGENTS.md`,
41
40
  `CLAUDE.md`, `CONTEXT.md`, and any `docs/adr/` entry the issue touches. Those
@@ -106,7 +105,8 @@ or the full test suite on this host. It is shared, and CI owns the heavy gates.
106
105
  ## Push and get to green
107
106
 
108
107
  Your checkout is a git repository of its own. Committing is local and always
109
- works; **publishing is the dispatcher's job**, because you hold no credential.
108
+ works; **publishing is the dispatcher's job**, and that is a rule, not a missing
109
+ credential.
110
110
 
111
111
  1. One review pass over your **whole** diff (`git diff origin/HEAD...HEAD`).
112
112
  Collect every finding, apply them all, then publish **once**.
@@ -115,7 +115,8 @@ works; **publishing is the dispatcher's job**, because you hold no credential.
115
115
  git add -A && git commit -m "<type>: <what changed>"
116
116
  ```
117
117
  No AI or co-author attribution. Never force-push. Never `git add -f`.
118
- Do not run `git push` it will fail, and it is not how your work ships.
118
+ Do not run `git push`. It is not how your work ships, and a push the
119
+ dispatcher did not make is one it cannot account for.
119
120
  3. Publish the branch with the `conductor_push` tool. The dispatcher fetches
120
121
  your commits out of this repository and pushes them to GitHub fast-forward
121
122
  only. If it reports a rejection, read the git error it hands back: a
@@ -142,7 +143,8 @@ works; **publishing is the dispatcher's job**, because you hold no credential.
142
143
 
143
144
  Editing, building and testing are yours: ordinary `bash` inside your worktree,
144
145
  unmediated. What leaves this machine is not. Four tools are the *only* route to
145
- GitHub, and you hold no credential that would let you take another one:
146
+ GitHub you may take another one exists on this host, and taking it is a
147
+ reportable breach rather than a shortcut:
146
148
 
147
149
  | Tool | What it does |
148
150
  | --- | --- |
@@ -162,8 +164,8 @@ Three things follow, and they are worth reading once rather than rediscovering:
162
164
  remote moved: fetch, rebase in your worktree, push again.
163
165
  - **A refusal is the real answer.** These tools decide in the dispatcher, not in
164
166
  this prompt, and every call is recorded. When one refuses, it says exactly
165
- why. Read it and act on it — do not retry it unchanged, and do not look for
166
- another way round it. There isn't one, and looking is itself reportable.
167
+ why. Read it and act on it — do not retry it unchanged, and do not go round it
168
+ with `gh` or `git push`. Going round a refusal is itself reportable.
167
169
 
168
170
  ## You do not merge, release, or deploy
169
171
 
package/src/cli.ts CHANGED
@@ -21,9 +21,8 @@ import {
21
21
  proposeRetrofit,
22
22
  repairPolicyBannerCrumbs,
23
23
  } from "./brief-upgrade.ts";
24
- import { findProject, loadConfig, resolveCaps, sharedRoot, stateDir } from "./config.ts";
24
+ import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
25
25
  import { CONDITION_FORMS, parseCondition } from "./decisions.ts";
26
- import { boundarySetupScript } from "./credentials.ts";
27
26
  import { runDaemon, setPaused } from "./daemon.ts";
28
27
  import {
29
28
  armTicks,
@@ -105,7 +104,6 @@ usage:
105
104
  omp-conductor pause
106
105
  omp-conductor resume
107
106
  omp-conductor graph-setup [--project NAME] [--write]
108
- omp-conductor boundary-setup [--slots N] [--daemon-user USER] [--shared-root PATH]
109
107
  omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
110
108
  omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
111
109
  omp-conductor report --text TEXT [--kind material|digest] [--project NAME]
@@ -202,14 +200,6 @@ usage:
202
200
  current. --write writes the two units and the script they run, and
203
201
  prints the systemctl line to run — it never runs systemctl itself.
204
202
  Exits 1 when no repo in the project has graphProject configured.
205
- boundary-setup
206
- print the root commands that provision the credential boundary
207
- (#125): the per-run slot accounts, the conductor-daemon and
208
- conductor-runs groups, the 0711 shared root, and the daemon home's
209
- mode. Prints; never runs them — it creates system accounts, so it is
210
- your sudo and your chance to read it first. Needs no config, and
211
- must run BEFORE setup, which writes worktree and mirror paths into
212
- the root this creates. --slots defaults to maxConcurrentWorkers.
213
203
  brief-upgrade
214
204
  inspect the brief overlay (package floor + POLICY.md). Reports by
215
205
  default. --migrate lifts a bannered ORCHESTRATOR.md owned half into
@@ -554,43 +544,6 @@ try {
554
544
  break;
555
545
  }
556
546
 
557
- case "boundary-setup": {
558
- // Printed, never executed: it creates system accounts, so it is the
559
- // operator's `sudo` and their chance to read it first — same posture as
560
- // `graph-setup`. Generated from the same constants the probe checks, so
561
- // the instructions cannot drift from what the daemon then demands (#125).
562
- //
563
- // Nothing is read from the config unless a flag was omitted, and that is
564
- // the point rather than an optimisation: per-run provisioning has to
565
- // happen BEFORE `setup`, because setup writes worktree and mirror paths
566
- // into the privileged shared root this command creates. Loading the
567
- // config unconditionally would be a bootstrap cycle — provision needs a
568
- // config, the config needs the provisioned root.
569
- const slotsFlag = flag(argv, "slots");
570
- let slots: number;
571
- if (slotsFlag === undefined) {
572
- const cfg = loadConfig();
573
- slots = resolveCaps(findProject(cfg, flag(argv, "project")), cfg.defaults).maxConcurrentWorkers;
574
- } else if (/^\d+$/.test(slotsFlag) && Number.parseInt(slotsFlag, 10) >= 1) {
575
- // Whole string, not a prefix: `parseInt("2workers")` is 2, and silently
576
- // provisioning a different pool than the operator typed is worse than
577
- // refusing.
578
- slots = Number.parseInt(slotsFlag, 10);
579
- } else {
580
- console.error(`boundary-setup: --slots must be a positive integer, got ${JSON.stringify(slotsFlag)}`);
581
- process.exitCode = 2;
582
- break;
583
- }
584
- // Both explicit where given, because neither can be inferred safely here.
585
- // Under `sudo` the invoking user is root, so `userInfo()` would provision
586
- // the wrong account and hand the daemon group to root; and the shared
587
- // root must be the *same* string the daemon later resolves, not one
588
- // derived from however this process happened to be invoked.
589
- const daemonUser = flag(argv, "daemon-user") ?? process.env["SUDO_USER"] ?? userInfo().username;
590
- const root = flag(argv, "shared-root") ?? sharedRoot();
591
- process.stdout.write(boundarySetupScript({ slots, sharedRoot: root, daemonUser }));
592
- break;
593
- }
594
547
  /**
595
548
  * The action ledger (#126): every conductor-verb call, what it asked for,
596
549
  * and how the daemon decided it.
package/src/config.ts CHANGED
@@ -20,7 +20,6 @@ import {
20
20
  BASE_FRESHNESS,
21
21
  BEHIND_BASE_ACTIONS,
22
22
  CONFIG_VERSION,
23
- CREDENTIAL_ISOLATIONS,
24
23
  DEFAULT_AUTHORITY,
25
24
  DEFAULT_CAPS,
26
25
  DEFAULT_PROJECT_POLICY,
@@ -36,8 +35,6 @@ import {
36
35
  REPORT_SCOPES,
37
36
  type Caps,
38
37
  type ConductorConfig,
39
- type CredentialConfig,
40
- type CredentialIsolation,
41
38
  type MergePreconditions,
42
39
  type PlanUsageCap,
43
40
  type ProjectConfig,
@@ -70,7 +67,6 @@ const AUTHORITY_HOLDER_LIST = quoteList(AUTHORITY_HOLDERS);
70
67
  const ORCHESTRATOR_MODE_LIST = quoteList(ORCHESTRATOR_MODES);
71
68
  const RELEASE_SHAPE_LIST = quoteList(RELEASE_SHAPES);
72
69
  const LEGACY_RELEASE_POLICY_LIST = quoteList(LEGACY_RELEASE_POLICIES);
73
- const CREDENTIAL_ISOLATION_LIST = quoteList(CREDENTIAL_ISOLATIONS);
74
70
  const BASE_FRESHNESS_LIST = quoteList(BASE_FRESHNESS);
75
71
  const DRAFT_POLICY_LIST = quoteList(DRAFT_POLICIES);
76
72
  const BEHIND_BASE_ACTION_LIST = quoteList(BEHIND_BASE_ACTIONS);
@@ -107,45 +103,6 @@ export function stateDir(): string {
107
103
  return dirname(configPath());
108
104
  }
109
105
 
110
- /**
111
- * Root of everything a run principal must reach: worktrees, mirrors, per-run
112
- * session transcripts and per-run boundary homes.
113
- *
114
- * **Outside the daemon's home, not a sibling of the state directory**, and both
115
- * halves of that are load-bearing:
116
- *
117
- * - It cannot live *inside* {@link stateDir}, which stays `0700` because it
118
- * holds `config.json`, `conductor.db` and the WAL/SHM files SQLite recreates
119
- * at runtime with the process umask. Making it searchable — all a slot needs
120
- * — would publish fleet, run and report history to every local account, and
121
- * chmod-ing the leaves once would not hold.
122
- * - It cannot live under the daemon's **home** either, which the boundary
123
- * requires to be `0700` so a slot cannot reach `gh` config, `~/.ssh` or
124
- * `~/.npmrc`. A shared root at `~/.omp/conductor-shared` is unreachable for
125
- * exactly the same reason, and the two hardening steps would silently cancel
126
- * each other out — the boundary would look correct and no run could start.
127
- *
128
- * So it is a system path: `0711` — traversable by a slot that already knows its
129
- * own path, listable by nobody, writable by nobody but the daemon. Only used
130
- * when a per-run boundary is active; an `isolation: "none"` fleet keeps every
131
- * path exactly where it was (#125).
132
- *
133
- * `$OMP_CONDUCTOR_SHARED` overrides it, which is how tests and non-FHS hosts
134
- * relocate it. Uninstall is two paths instead of one, and the README says so.
135
- */
136
- export function sharedRoot(): string {
137
- const override = process.env["OMP_CONDUCTOR_SHARED"];
138
- if (override !== undefined && override.length > 0) return expandHome(override);
139
- // Deliberately NOT derived from whether `$OMP_CONDUCTOR_HOME` happens to be
140
- // set: generated systemd units set it even for a default install, so keying
141
- // the security topology off it lets the CLI, setup and the daemon each pick a
142
- // different root depending on how they were invoked. One explicit override,
143
- // or the platform default.
144
- // Not `/tmp`: a shared root a slot could rename or replace would hand one run
145
- // control of another's checkout path.
146
- return process.platform === "linux" ? "/var/lib/omp-conductor" : `${stateDir()}-shared`;
147
- }
148
-
149
106
  /**
150
107
  * Where per-run worktrees and bare mirrors live when a project names neither.
151
108
  *
@@ -154,14 +111,6 @@ export function sharedRoot(): string {
154
111
  * worker checkouts even when the config was too broken to load and say where
155
112
  * they are. Two copies of this default would mean a jail guarding the wrong
156
113
  * directory on exactly the day the config is broken.
157
- *
158
- * `base` is what makes these usable under a per-run boundary. A slot principal
159
- * cannot traverse the `0700` state directory, and dispatch now refuses rather
160
- * than widening it — so an isolated fleet whose roots defaulted under the state
161
- * dir would provision a repository and then fail before launching anything.
162
- * Setup passes {@link sharedRoot} for those fleets and `stateDir()` for the
163
- * rest, so an `isolation: "none"` install keeps every path exactly where it was
164
- * (#125).
165
114
  */
166
115
  export function defaultWorkspaceRoot(base: string = stateDir()): string {
167
116
  return join(base, "worktrees");
@@ -170,18 +119,6 @@ export function defaultWorkspaceRoot(base: string = stateDir()): string {
170
119
  export function defaultMirrorRoot(base: string = stateDir()): string {
171
120
  return join(base, "mirrors");
172
121
  }
173
- /**
174
- * The root a project's slot-accessible trees belong under, given its isolation.
175
- *
176
- * Only `per-run` moves. `group-mode` runs sessions as the daemon's *own* uid —
177
- * it separates checkouts by group and mode, not by principal — so it can reach
178
- * the state directory perfectly well, and sending it to a system path would
179
- * demand privileged provisioning a group-mode host may deliberately not have.
180
- * `none` never moves either.
181
- */
182
- export function projectTreeBase(isolation: CredentialIsolation): string {
183
- return isolation === "per-run" ? sharedRoot() : stateDir();
184
- }
185
122
 
186
123
  /**
187
124
  * Reads, validates and normalises the config. Throws an `Error` naming the
@@ -481,7 +418,6 @@ function normalizeProject(
481
418
  const escalation = normalizeEscalation(raw["escalation"], label, problems);
482
419
  const authority = normalizeAuthority(raw["authority"], label, problems);
483
420
  const releasePolicy = normalizeReleasePolicy(raw["releasePolicy"], label, problems);
484
- const credentials = normalizeCredentials(raw["credentials"], label, problems);
485
421
  const policy = normalizeProjectPolicy(raw["policy"], label, problems);
486
422
 
487
423
  const caps = coerceCaps(raw["caps"], `${label}: caps`, problems, legacyCaps);
@@ -510,18 +446,9 @@ function normalizeProject(
510
446
  authority,
511
447
  releasePolicy,
512
448
  policy,
513
- credentials,
514
449
  reporting,
515
- // Derived from this project's own isolation, not from `stateDir()`: a
516
- // hand-written `per-run` config that omits these optional keys would
517
- // otherwise resolve under the 0700 private tree and be refused at dispatch,
518
- // having never gone through the setup wizard that knows better (#125).
519
- workspaceRoot: expandHome(
520
- pickString(raw["workspaceRoot"], defaultWorkspaceRoot(projectTreeBase(credentials.isolation))),
521
- ),
522
- mirrorRoot: expandHome(
523
- pickString(raw["mirrorRoot"], defaultMirrorRoot(projectTreeBase(credentials.isolation))),
524
- ),
450
+ workspaceRoot: expandHome(pickString(raw["workspaceRoot"], defaultWorkspaceRoot())),
451
+ mirrorRoot: expandHome(pickString(raw["mirrorRoot"], defaultMirrorRoot())),
525
452
  };
526
453
  }
527
454
 
@@ -841,159 +768,15 @@ function normalizeNameList(parsed: unknown, at: string, problems: string[]): str
841
768
  return names;
842
769
  }
843
770
 
844
-
845
- /**
846
- * The credential boundary, and the one migration that must never brick a fleet.
847
- *
848
- * `omp/systemd/omp-conductor.service.example` runs the daemon as an
849
- * unprivileged account. A naive "fail closed unless per-run uids are available"
850
- * release would therefore stop every *existing* fleet from dispatching on
851
- * upgrade — a production outage caused by a security feature, which is a worse
852
- * outcome than the day before. The issue's rule ("never the silent default")
853
- * still holds; it is satisfied by a **migration** rather than by a default:
854
- *
855
- * 1. Key present → honoured exactly.
856
- * 2. Key absent → resolves to `"none"`, and `migrateCredentialsOnDisk` rewrites
857
- * the file so the answer is explicit on disk, exactly the way the v1→v2 caps
858
- * migration normalises on load. The operator ends up with
859
- * `"isolation": "none"` in their config plus one log line naming it, and
860
- * nothing stops dispatching.
861
- * 3. A new config from `setup` asks, and only offers `per-run` as the default
862
- * when the host capability probe passes.
863
- *
864
- * A malformed value is rejected rather than folded to `"none"`, for the reason
865
- * every other vocabulary here is: a misspelt `"per_run"` that silently resolved
866
- * to unprotected would read as configured in the file while the fleet ran open.
867
- */
868
- function normalizeCredentials(parsed: unknown, label: string, problems: string[]): CredentialConfig {
869
- if (parsed === undefined) return { isolation: "none" };
870
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
871
- problems.push(
872
- `${label}: credentials must be an object with an "isolation" of ${CREDENTIAL_ISOLATION_LIST}, ` +
873
- `found ${JSON.stringify(parsed)}`,
874
- );
875
- return { isolation: "none" };
876
- }
877
- const raw = parsed as Raw;
878
- const isolation = pickLiteral(
879
- raw["isolation"],
880
- CREDENTIAL_ISOLATIONS,
881
- "none",
882
- `${label}: credentials.isolation`,
883
- CREDENTIAL_ISOLATION_LIST,
884
- problems,
885
- );
886
- const rawToken = raw["readToken"];
887
- if (rawToken !== undefined && !nonEmptyString(rawToken)) {
888
- problems.push(
889
- `${label}: credentials.readToken must be a non-empty string when present — an empty token is not ` +
890
- `"no token", it is a token that fails every call`,
891
- );
892
- }
893
- return {
894
- isolation,
895
- ...(nonEmptyString(rawToken) ? { readToken: rawToken } : {}),
896
- };
897
- }
898
-
899
- /**
900
- * The effective boundary for a project. `loadConfig` always writes one, so this
901
- * only matters for a `ProjectConfig` some caller hand-built — and it resolves
902
- * the same way the loader does rather than inventing a second answer.
903
- */
904
- export function resolveCredentials(p: ProjectConfig): CredentialConfig {
905
- return p.credentials ?? { isolation: "none" };
906
- }
907
-
908
- /**
909
- * Named projects whose on-disk config predates `credentials` — the input to the
910
- * migration, and pure so it can be tested against a raw object.
911
- */
912
- export function projectsMissingCredentials(parsed: unknown): string[] {
913
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return [];
914
- const projects = (parsed as Raw)["projects"];
915
- if (!Array.isArray(projects)) return [];
916
- const missing: string[] = [];
917
- projects.forEach((p: unknown, i) => {
918
- if (typeof p !== "object" || p === null || Array.isArray(p)) return;
919
- const raw = p as Raw;
920
- const credentials = raw["credentials"];
921
- const explicit =
922
- typeof credentials === "object" &&
923
- credentials !== null &&
924
- !Array.isArray(credentials) &&
925
- (credentials as Raw)["isolation"] !== undefined;
926
- if (!explicit) missing.push(nonEmptyString(raw["name"]) ? raw["name"] : `projects[${String(i)}]`);
927
- });
928
- return missing;
929
- }
930
-
931
- /**
932
- * Write the migration through, once. Returns the projects that gained an
933
- * explicit key, so the caller can log it by name.
934
- *
935
- * Separate from `loadConfig` on purpose: `loadConfig` is read-only and is called
936
- * by every CLI surface, and a loader with a write side-effect is one an
937
- * operator cannot run against a config they are only inspecting. A failure to
938
- * persist is logged by the caller and is **not** fatal — the in-memory value is
939
- * already `"none"`, and refusing to boot because the config file is read-only
940
- * would be exactly the outage this migration exists to avoid.
941
- */
942
- export function migrateCredentialsOnDisk(): { migrated: string[]; path: string } {
943
- const path = configPath();
944
- let parsed: unknown;
945
- try {
946
- parsed = JSON.parse(readFileSync(path, "utf8"));
947
- } catch {
948
- return { migrated: [], path };
949
- }
950
- const migrated = projectsMissingCredentials(parsed);
951
- if (migrated.length === 0) return { migrated: [], path };
952
-
953
- // A SURGICAL edit: add the one missing key to the projects that lack it and
954
- // leave every other byte exactly as the operator wrote it.
955
- //
956
- // This was `saveConfig(loadConfig())` once, and that round-trip took a live
957
- // fleet down. Normalising on the way out rewrites the whole file into the
958
- // current dialect — `defaults.planUsage`, `releasePolicy` as a per-shape map
959
- // — and the previous release rejects both. The daemon performing the
960
- // migration is started *by* an upgrade still running the previous CLI, so
961
- // that CLI's next `loadConfig` failed, and its rollback reinstalled a version
962
- // which then could not read the file either. A migration must never make a
963
- // config unreadable by the release you may have to roll back to.
964
- // Verified against the real previous release, not just against this parser:
965
- // omp-conductor@0.3.25 loads a config carrying `credentials` and prints
966
- // `status` normally, because it ignores unknown *project* keys while
967
- // rejecting unknown `defaults` keys and a non-string `releasePolicy` — the
968
- // two things the old round-trip wrote. Re-check that with `npm install
969
- // omp-conductor@<previous>` before widening what this writes.
970
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { migrated: [], path };
971
- const projects = (parsed as Raw)["projects"];
972
- if (!Array.isArray(projects)) return { migrated: [], path };
973
- for (const entry of projects) {
974
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
975
- const raw = entry as Raw;
976
- const credentials = raw["credentials"];
977
- const explicit =
978
- typeof credentials === "object" &&
979
- credentials !== null &&
980
- !Array.isArray(credentials) &&
981
- (credentials as Raw)["isolation"] !== undefined;
982
- if (!explicit) (raw as Record<string, unknown>)["credentials"] = { isolation: "none" };
983
- }
984
- writeConfigFile(parsed);
985
- return { migrated, path };
986
- }
987
-
988
771
  /**
989
- * A clone URL carrying credentials voids the entire boundary (#125).
772
+ * A clone URL carrying a password or token is rejected at load.
990
773
  *
991
774
  * `git clone` persists whatever is in the URL into the mirror's config, and the
992
775
  * run repository inherits `origin` from it — so a `https://<pat>@github.com/…`
993
- * hands every session the write credential the rest of this release removes,
994
- * through a file no environment scrub touches. This used to be a `ponytail`
995
- * note on `worktree.ts`'s `ensureMirror`; a warning is not a boundary, so it is
996
- * now rejected at load with the field named.
776
+ * writes the operator's credential into a file on disk that every later run
777
+ * reads, where nobody is looking for it. This used to be a `ponytail` note on
778
+ * `worktree.ts`'s `ensureMirror`; a warning nobody reads is not a check, so it
779
+ * is now rejected at load with the field named.
997
780
  *
998
781
  * An SSH URL with a plain username (`ssh://git@github.com/o/r`, `git@github.com:o/r`)
999
782
  * is not a credential and is left alone — that is how nearly every fleet is
@@ -20,9 +20,8 @@
20
20
  * `bash` is deliberately not confined here either: its input is an opaque shell
21
21
  * string, and parsing it is a false sense of security. This gate does not
22
22
  * contain a determined session, and no comment or doc here should claim
23
- * otherwise — closing that gap is a least-privilege OS principal (a worker uid
24
- * under `credentials.isolation: "per-run"`), documented beside this module's
25
- * README section, not a regex over `rm -rf`.
23
+ * otherwise — closing that gap would take a least-privilege OS principal for
24
+ * the worker, not a regex over `rm -rf`.
26
25
  *
27
26
  * What it does buy is a *legible* refusal that holds on hosts where no uid
28
27
  * split will ever be deployed: a structured `edit` outside the assigned