@tiphys/kernel 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/AGENTS.md +611 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +278 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +14 -0
  11. package/dist/src/brief.d.ts +32 -0
  12. package/dist/src/brief.js +59 -0
  13. package/dist/src/checklists.d.ts +169 -0
  14. package/dist/src/checklists.js +310 -0
  15. package/dist/src/checks.d.ts +828 -0
  16. package/dist/src/checks.js +3314 -0
  17. package/dist/src/cli.d.ts +9 -0
  18. package/dist/src/cli.js +57 -0
  19. package/dist/src/commands/brief.d.ts +92 -0
  20. package/dist/src/commands/brief.js +320 -0
  21. package/dist/src/commands/checklist.d.ts +42 -0
  22. package/dist/src/commands/checklist.js +168 -0
  23. package/dist/src/commands/doctor.d.ts +35 -0
  24. package/dist/src/commands/doctor.js +605 -0
  25. package/dist/src/commands/gates.d.ts +9 -0
  26. package/dist/src/commands/gates.js +360 -0
  27. package/dist/src/commands/init.d.ts +30 -0
  28. package/dist/src/commands/init.js +128 -0
  29. package/dist/src/commands/lock.d.ts +1 -0
  30. package/dist/src/commands/lock.js +229 -0
  31. package/dist/src/commands/mode.d.ts +40 -0
  32. package/dist/src/commands/mode.js +134 -0
  33. package/dist/src/commands/plan.d.ts +20 -0
  34. package/dist/src/commands/plan.js +105 -0
  35. package/dist/src/commands/pool.d.ts +1 -0
  36. package/dist/src/commands/pool.js +128 -0
  37. package/dist/src/commands/spawn.d.ts +1 -0
  38. package/dist/src/commands/spawn.js +146 -0
  39. package/dist/src/commands/status.d.ts +19 -0
  40. package/dist/src/commands/status.js +120 -0
  41. package/dist/src/commands/teardown.d.ts +1 -0
  42. package/dist/src/commands/teardown.js +79 -0
  43. package/dist/src/commands/tuition.d.ts +21 -0
  44. package/dist/src/commands/tuition.js +218 -0
  45. package/dist/src/commands/validate.d.ts +78 -0
  46. package/dist/src/commands/validate.js +360 -0
  47. package/dist/src/commands/watch.d.ts +1 -0
  48. package/dist/src/commands/watch.js +172 -0
  49. package/dist/src/exec/env.d.ts +128 -0
  50. package/dist/src/exec/env.js +190 -0
  51. package/dist/src/fleet.d.ts +51 -0
  52. package/dist/src/fleet.js +80 -0
  53. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  54. package/dist/src/gates/adapters/http-json.js +283 -0
  55. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  56. package/dist/src/gates/adapters/migrations-command.js +373 -0
  57. package/dist/src/gates/citations.d.ts +408 -0
  58. package/dist/src/gates/citations.js +1163 -0
  59. package/dist/src/gates/coverage.d.ts +284 -0
  60. package/dist/src/gates/coverage.js +701 -0
  61. package/dist/src/gates/credentials.d.ts +74 -0
  62. package/dist/src/gates/credentials.js +533 -0
  63. package/dist/src/gates/deploy.d.ts +1 -0
  64. package/dist/src/gates/deploy.js +33 -0
  65. package/dist/src/gates/manifest.d.ts +99 -0
  66. package/dist/src/gates/manifest.js +208 -0
  67. package/dist/src/gates/migrations.d.ts +1 -0
  68. package/dist/src/gates/migrations.js +36 -0
  69. package/dist/src/gates/pin.d.ts +114 -0
  70. package/dist/src/gates/pin.js +154 -0
  71. package/dist/src/gates/red-witness.d.ts +22 -0
  72. package/dist/src/gates/red-witness.js +390 -0
  73. package/dist/src/gates/release.d.ts +283 -0
  74. package/dist/src/gates/release.js +820 -0
  75. package/dist/src/gates/result.d.ts +116 -0
  76. package/dist/src/gates/result.js +91 -0
  77. package/dist/src/gates/run.d.ts +566 -0
  78. package/dist/src/gates/run.js +1536 -0
  79. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  80. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  81. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  82. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  83. package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
  84. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  85. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  86. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  87. package/dist/src/gates/scope.d.ts +131 -0
  88. package/dist/src/gates/scope.js +1018 -0
  89. package/dist/src/gates/suite.d.ts +217 -0
  90. package/dist/src/gates/suite.js +927 -0
  91. package/dist/src/gates/validate.d.ts +121 -0
  92. package/dist/src/gates/validate.js +414 -0
  93. package/dist/src/hooks.d.ts +32 -0
  94. package/dist/src/hooks.js +62 -0
  95. package/dist/src/liveness.d.ts +321 -0
  96. package/dist/src/liveness.js +396 -0
  97. package/dist/src/lock.d.ts +178 -0
  98. package/dist/src/lock.js +500 -0
  99. package/dist/src/modes.d.ts +149 -0
  100. package/dist/src/modes.js +258 -0
  101. package/dist/src/path-identity.d.ts +2 -0
  102. package/dist/src/path-identity.js +10 -0
  103. package/dist/src/plan.d.ts +73 -0
  104. package/dist/src/plan.js +153 -0
  105. package/dist/src/pool.d.ts +130 -0
  106. package/dist/src/pool.js +721 -0
  107. package/dist/src/roles.d.ts +430 -0
  108. package/dist/src/roles.js +734 -0
  109. package/dist/src/spawn.d.ts +177 -0
  110. package/dist/src/spawn.js +332 -0
  111. package/dist/src/status.d.ts +91 -0
  112. package/dist/src/status.js +119 -0
  113. package/dist/src/task.d.ts +264 -0
  114. package/dist/src/task.js +305 -0
  115. package/dist/src/teardown.d.ts +32 -0
  116. package/dist/src/teardown.js +314 -0
  117. package/dist/src/tuition.d.ts +159 -0
  118. package/dist/src/tuition.js +311 -0
  119. package/dist/src/validate.d.ts +230 -0
  120. package/dist/src/validate.js +732 -0
  121. package/dist/src/version.d.ts +3 -0
  122. package/dist/src/version.js +38 -0
  123. package/dist/src/watcher.d.ts +275 -0
  124. package/dist/src/watcher.js +859 -0
  125. package/dist/src/witness/run.d.ts +274 -0
  126. package/dist/src/witness/run.js +1327 -0
  127. package/dist/src/witness/spec.d.ts +102 -0
  128. package/dist/src/witness/spec.js +253 -0
  129. package/dist/tsconfig.src.tsbuildinfo +1 -0
  130. package/gate-registry.yaml +390 -0
  131. package/gates.manifest.json +195 -0
  132. package/package.json +57 -3
  133. package/role-model-config.yaml +88 -0
  134. package/roles/README.md +128 -0
  135. package/roles/_shared-dispatch-contract.md +87 -0
  136. package/roles/adversarial-plan-reviewer.md +80 -0
  137. package/roles/clean-room-reviewer.md +140 -0
  138. package/roles/implementer.md +460 -0
  139. package/roles/investigator.md +138 -0
  140. package/roles/plan-writer.md +95 -0
  141. package/schemas/README.md +81 -0
  142. package/schemas/assurance-modes.schema.json +264 -0
  143. package/schemas/charter.schema.json +166 -0
  144. package/schemas/checklist.schema.json +114 -0
  145. package/schemas/decision-record.schema.json +88 -0
  146. package/schemas/final-report.schema.json +90 -0
  147. package/schemas/finding.schema.json +106 -0
  148. package/schemas/gate-registry.schema.json +260 -0
  149. package/schemas/mechanism-index.schema.json +94 -0
  150. package/schemas/plan.schema.json +300 -0
  151. package/schemas/report.schema.json +579 -0
  152. package/schemas/role-brief.schema.json +105 -0
  153. package/schemas/role-model-config.schema.json +90 -0
  154. package/schemas/status-line.schema.json +40 -0
  155. package/schemas/tuition.schema.json +191 -0
  156. package/schemas/verdict.schema.json +289 -0
  157. package/schemas/work-history.schema.json +183 -0
  158. package/templates/charter.example.yaml +54 -0
  159. package/templates/decision-record.example.yaml +27 -0
  160. package/templates/final-report.example.yaml +80 -0
  161. package/templates/plan.example.yaml +87 -0
  162. package/templates/report.example.yaml +236 -0
  163. package/templates/warnings.md +74 -0
  164. package/templates/work-history.example.yaml +185 -0
  165. package/tuition/README.md +76 -0
  166. package/tuition/T-001.yaml +48 -0
  167. package/tuition/T-002.yaml +51 -0
  168. package/tuition/T-003.yaml +100 -0
  169. package/tuition/T-004.yaml +52 -0
  170. package/tuition/T-005.yaml +72 -0
  171. package/tuition/T-006.yaml +81 -0
  172. package/tuition/T-007.yaml +56 -0
  173. package/tuition/T-008.yaml +111 -0
  174. package/tuition/T-009.yaml +50 -0
  175. package/tuition/T-015.yaml +36 -0
  176. package/tuition/T-016.yaml +36 -0
  177. package/tuition/T-017.yaml +46 -0
  178. package/tuition/T-018.yaml +84 -0
  179. package/tuition/T-021.yaml +40 -0
  180. package/tuition/T-022.yaml +36 -0
  181. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,74 @@
1
+ import type { GateStatus } from "./result.ts";
2
+ /** What one source probe concluded. */
3
+ export interface SourceProbe {
4
+ /** The credential source's stable name. */
5
+ source: string;
6
+ outcome: "clean" | "resolvable" | "error";
7
+ detail: string;
8
+ }
9
+ /**
10
+ * gh's documented token vocabulary (see the module comment for the walk).
11
+ * Never permitted in a child environment, allowlisted or not.
12
+ */
13
+ export declare const GH_TOKEN_VARIABLES: readonly string[];
14
+ /**
15
+ * The credential- or code-execution-capable environment vocabulary of the
16
+ * programs a scrubbed child runs, walked from each program's own
17
+ * documentation. A child environment must contain NONE of these,
18
+ * allowlisted or not; the environment probe reddens if any is present.
19
+ * This is a BOUNDED denylist (it cannot list every dangerous name any
20
+ * program will ever read); the allowlist in src/exec/env.ts remains the
21
+ * real defense. Per-name source:
22
+ *
23
+ * git (git-config(1), git(1)):
24
+ * GIT_ASKPASS - program git runs to obtain a password.
25
+ * GIT_SSH_COMMAND - shell command git uses for its ssh transport.
26
+ * GIT_PROXY_COMMAND - external program for git:// connections.
27
+ * GIT_CONFIG_COUNT - trigger of the environment config-injection
28
+ * family; with GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n
29
+ * it injects arbitrary config (e.g. a
30
+ * credential.helper) at a scope --global/--system
31
+ * cannot see. The numbered KEY_n / VALUE_n members
32
+ * are matched by isDangerousEnvName's pattern.
33
+ * ssh (ssh(1), ssh-add(1)):
34
+ * SSH_ASKPASS - program ssh runs to obtain a passphrase.
35
+ * node / dynamic loader / shell startup (node(1), ld.so(8), bash(1)):
36
+ * NODE_OPTIONS - options node applies at startup (can require
37
+ * arbitrary modules), arbitrary code execution.
38
+ * NODE_EXTRA_CA_CERTS- extra CAs node trusts, a TLS-trust channel.
39
+ * LD_PRELOAD - shared objects the loader injects into every
40
+ * dynamically linked program, arbitrary code.
41
+ * BASH_ENV - script bash sources at non-interactive startup.
42
+ * ENV - script the POSIX shell sources at startup.
43
+ *
44
+ * GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM are DELIBERATELY absent: they are
45
+ * redirected (CREDENTIAL_STORE_REDIRECTIONS) to empty harness-owned targets
46
+ * and the override runs last in buildChildEnv, so their mere presence is
47
+ * expected and neutralized; flagging them would false-positive on every
48
+ * real run.
49
+ */
50
+ export declare const DANGEROUS_ENV_VOCABULARY: readonly string[];
51
+ /** Whether a variable name is in the walked dangerous vocabulary. */
52
+ export declare function isDangerousEnvName(name: string): boolean;
53
+ /** The names credential-scrub probes, in probe order. */
54
+ export declare const CREDENTIAL_SOURCES: readonly string[];
55
+ export interface ProbeOptions {
56
+ /**
57
+ * The variable names the environment source accepts. Defaults to the
58
+ * constructed contract (allowlist plus redirections). Passed explicitly
59
+ * by tests staging dangerous states.
60
+ */
61
+ permittedNames?: ReadonlySet<string>;
62
+ }
63
+ /**
64
+ * Probe every credential source FROM INSIDE the given child environment.
65
+ * The environment is the one under test: every subprocess probe below runs
66
+ * with `env` as its entire environment, so what is asserted is what a
67
+ * child launched with this environment could actually resolve.
68
+ */
69
+ export declare function probeCredentialSources(env: Record<string, string | undefined>, options?: ProbeOptions): SourceProbe[];
70
+ /** Fold probes into a gate verdict. Any error wins over any red. */
71
+ export declare function verdictFromProbes(probes: SourceProbe[]): {
72
+ status: GateStatus;
73
+ detail: string;
74
+ };
@@ -0,0 +1,533 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync, writeFileSync } from "node:fs";
3
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { pathsIdentifySameObject } from "../path-identity.js";
6
+ import { buildChildEnv, permittedChildEnvNames, } from "../exec/env.js";
7
+ import { readRegularFileIfPresent, refuseOpenForWrite, runStep, singleLine, } from "../task.js";
8
+ import { exitCodeForStatus, makeGateResult, renderGateResult, } from "./result.js";
9
+ /**
10
+ * THE CREDENTIAL GATES (kernel plan M2, M2-P8 step 6).
11
+ *
12
+ * Two registered entries, both invoked through the M2-P1 gate subprocess
13
+ * contract (`node src/gates/credentials.ts <gate-id> --result <path>
14
+ * --evidence <dir>`), each writing exactly one GateResult.
15
+ *
16
+ * `credential-scrub` (required, offline). Makes "implementers never create
17
+ * PRs" PROBED rather than believed: it constructs a child environment with
18
+ * the SAME `buildChildEnv` the executor uses (one mechanism, per T-005,
19
+ * never a second implementation) and then probes, FROM INSIDE that
20
+ * environment, every source a pull-request-capable credential could be
21
+ * resolved from. `units` is the number of SOURCES PROBED, never the number
22
+ * of variable names checked: under an allowlist no excluded name can
23
+ * survive by construction, so a name count is a tautology that grows by
24
+ * adding names and measures nothing (M2R-004).
25
+ *
26
+ * THE DERIVED DENYLIST TRIPWIRE IN THIS MODULE, AND WHY IT IS PERMITTED
27
+ * HERE, AND EXACTLY WHAT IT DOES AND DOES NOT COVER.
28
+ * The environment-source probe carries a bounded, allowlist-INDEPENDENT
29
+ * tripwire: it reddens if a documented credential- or code-execution-
30
+ * capable variable is present in the constructed child, even if some
31
+ * future edit puts one on the allowlist in src/exec/env.ts. MECHANISMS.md's
32
+ * row on denylists allows exactly this shape: "where a denylist is
33
+ * unavoidable, DERIVE it by walking the consuming program's closed
34
+ * documented vocabulary once, publishing the walk." The walk covers the
35
+ * vocabularies of the programs a scrubbed child actually runs:
36
+ *
37
+ * - gh: `gh help environment` documents the token variables as GH_TOKEN,
38
+ * GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN
39
+ * (GH_TOKEN_VARIABLES).
40
+ * - git, ssh, node/loader/shell: the askpass, ssh-exec, proxy, config-
41
+ * injection and code-execution variables git-config(1), git(1),
42
+ * ssh(1), node(1) and bash(1) document (DANGEROUS_ENV_VOCABULARY;
43
+ * see that constant for the per-name source).
44
+ *
45
+ * WHY THIS IS A TRIPWIRE, NOT THE SCRUB. The scrub is the allowlist in
46
+ * src/exec/env.ts: nothing outside it can appear in a child by
47
+ * construction, so the allowlist is the real defense. This tripwire is a
48
+ * BOUNDED denylist and cannot enumerate every dangerous name that any
49
+ * program will ever read. What it buys is that "an allowlist widened by an
50
+ * implementer to turn a red gate green" (this phase's declared hazard)
51
+ * costs a red for every name in the walked vocabulary, not just gh's four
52
+ * tokens. A name outside the vocabulary that is also admitted to the
53
+ * allowlist would pass the environment probe green; that residue is the
54
+ * allowlist's responsibility, and this comment states it rather than
55
+ * hiding it. This is also the check M2-P7 step 8 reserves for the M4-era
56
+ * per-invocation extension ("the extension may never include a
57
+ * pull-request-capable credential").
58
+ *
59
+ * THE ENV-INJECTION VECTOR IS BEHAVIORALLY PROBED, NOT JUST NAME-CHECKED.
60
+ * git resolves a credential.helper injected via the GIT_CONFIG_COUNT /
61
+ * GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n family (git-config(1)) at a scope
62
+ * the --global and --system probes cannot see. So beyond the name
63
+ * tripwire, source `git-resolved-config` asks git itself, with NO scope
64
+ * flag, what credential.helper resolves from inside the child (env, then
65
+ * global, then system, then any repo-local config of the child's working
66
+ * directory). A resolvable helper from ANY source, including env
67
+ * injection, reddens the gate.
68
+ *
69
+ * `credential-token` (conditional). When TIPHYS_IMPLEMENTER_TOKEN is
70
+ * absent it reports `not-applicable` NAMING OWNER ACTION A-3, never green.
71
+ * When the token is present it currently reports `error`, deliberately:
72
+ * plan step 7 requires the probe's assertion to be DERIVED from captured
73
+ * API responses for a real scoped implementer token and a real
74
+ * orchestrator token, both of which owner action A-3 (DR-0004 item 4) has
75
+ * not yet provisioned. Deriving the assertion from invented responses is
76
+ * exactly the mechanism T-003 lesson 4 forbids, so until the captures
77
+ * exist the gate FAILS CLOSED (M2-C-3: a check that cannot reach a
78
+ * verdict is `error`). The live witness is deferred to the M2 exit test
79
+ * (plan criterion 7, owner-blocked).
80
+ */
81
+ const EX_USAGE = 64; // BSD sysexits, same value src/cli.ts exports.
82
+ /**
83
+ * gh's documented token vocabulary (see the module comment for the walk).
84
+ * Never permitted in a child environment, allowlisted or not.
85
+ */
86
+ export const GH_TOKEN_VARIABLES = [
87
+ "GH_TOKEN",
88
+ "GITHUB_TOKEN",
89
+ "GH_ENTERPRISE_TOKEN",
90
+ "GITHUB_ENTERPRISE_TOKEN",
91
+ ];
92
+ /**
93
+ * The credential- or code-execution-capable environment vocabulary of the
94
+ * programs a scrubbed child runs, walked from each program's own
95
+ * documentation. A child environment must contain NONE of these,
96
+ * allowlisted or not; the environment probe reddens if any is present.
97
+ * This is a BOUNDED denylist (it cannot list every dangerous name any
98
+ * program will ever read); the allowlist in src/exec/env.ts remains the
99
+ * real defense. Per-name source:
100
+ *
101
+ * git (git-config(1), git(1)):
102
+ * GIT_ASKPASS - program git runs to obtain a password.
103
+ * GIT_SSH_COMMAND - shell command git uses for its ssh transport.
104
+ * GIT_PROXY_COMMAND - external program for git:// connections.
105
+ * GIT_CONFIG_COUNT - trigger of the environment config-injection
106
+ * family; with GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n
107
+ * it injects arbitrary config (e.g. a
108
+ * credential.helper) at a scope --global/--system
109
+ * cannot see. The numbered KEY_n / VALUE_n members
110
+ * are matched by isDangerousEnvName's pattern.
111
+ * ssh (ssh(1), ssh-add(1)):
112
+ * SSH_ASKPASS - program ssh runs to obtain a passphrase.
113
+ * node / dynamic loader / shell startup (node(1), ld.so(8), bash(1)):
114
+ * NODE_OPTIONS - options node applies at startup (can require
115
+ * arbitrary modules), arbitrary code execution.
116
+ * NODE_EXTRA_CA_CERTS- extra CAs node trusts, a TLS-trust channel.
117
+ * LD_PRELOAD - shared objects the loader injects into every
118
+ * dynamically linked program, arbitrary code.
119
+ * BASH_ENV - script bash sources at non-interactive startup.
120
+ * ENV - script the POSIX shell sources at startup.
121
+ *
122
+ * GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM are DELIBERATELY absent: they are
123
+ * redirected (CREDENTIAL_STORE_REDIRECTIONS) to empty harness-owned targets
124
+ * and the override runs last in buildChildEnv, so their mere presence is
125
+ * expected and neutralized; flagging them would false-positive on every
126
+ * real run.
127
+ */
128
+ export const DANGEROUS_ENV_VOCABULARY = [
129
+ "GIT_ASKPASS",
130
+ "GIT_SSH_COMMAND",
131
+ "GIT_PROXY_COMMAND",
132
+ "GIT_CONFIG_COUNT",
133
+ "SSH_ASKPASS",
134
+ "NODE_OPTIONS",
135
+ "NODE_EXTRA_CA_CERTS",
136
+ "LD_PRELOAD",
137
+ "BASH_ENV",
138
+ "ENV",
139
+ ];
140
+ /**
141
+ * git-config(1)'s numbered environment config-injection members:
142
+ * GIT_CONFIG_KEY_<n> and GIT_CONFIG_VALUE_<n> for n in [0, COUNT). The
143
+ * index is git's own documented closed shape (a non-negative integer), so
144
+ * this is a vocabulary match, not a widened guess.
145
+ */
146
+ const GIT_CONFIG_INJECTION_MEMBER = /^GIT_CONFIG_(KEY|VALUE)_\d+$/;
147
+ /** Whether a variable name is in the walked dangerous vocabulary. */
148
+ export function isDangerousEnvName(name) {
149
+ return (DANGEROUS_ENV_VOCABULARY.includes(name) ||
150
+ GIT_CONFIG_INJECTION_MEMBER.test(name));
151
+ }
152
+ /** The names credential-scrub probes, in probe order. */
153
+ export const CREDENTIAL_SOURCES = [
154
+ "environment",
155
+ "gh-configuration",
156
+ "git-global-config",
157
+ "git-system-config",
158
+ "git-resolved-config",
159
+ "netrc",
160
+ "git-credentials",
161
+ ];
162
+ function probe(source, outcome, detail) {
163
+ return { source, outcome, detail };
164
+ }
165
+ /** A regular file with content at a credential-store path. */
166
+ function fileProbe(source, paths) {
167
+ const found = [];
168
+ for (const path of paths) {
169
+ // M2-C-6: the type is established before the open, and a path that is
170
+ // present but not a readable regular file is `error`, never guessed
171
+ // clean and never blocked on.
172
+ const read = readRegularFileIfPresent(path);
173
+ if (read.kind === "refused") {
174
+ return probe(source, "error", read.reason);
175
+ }
176
+ if (read.kind === "read" && read.body.length > 0) {
177
+ found.push(`${path} (${String(read.body.length)} bytes)`);
178
+ }
179
+ }
180
+ if (found.length > 0) {
181
+ return probe(source, "resolvable", `credential store reachable from inside the child environment: ${found.join(", ")}`);
182
+ }
183
+ return probe(source, "clean", `no populated store at ${paths.join(", ")}`);
184
+ }
185
+ /** Locate an executable on a PATH string, or undefined. */
186
+ function findOnPath(pathValue, program) {
187
+ if (pathValue === undefined) {
188
+ return undefined;
189
+ }
190
+ for (const dir of pathValue.split(delimiter)) {
191
+ if (dir === "") {
192
+ continue;
193
+ }
194
+ const candidate = join(dir, program);
195
+ try {
196
+ accessSync(candidate, constants.X_OK);
197
+ return candidate;
198
+ }
199
+ catch {
200
+ // Not here; keep walking.
201
+ }
202
+ }
203
+ return undefined;
204
+ }
205
+ /**
206
+ * Probe every credential source FROM INSIDE the given child environment.
207
+ * The environment is the one under test: every subprocess probe below runs
208
+ * with `env` as its entire environment, so what is asserted is what a
209
+ * child launched with this environment could actually resolve.
210
+ */
211
+ export function probeCredentialSources(env, options = {}) {
212
+ const permitted = options.permittedNames ?? permittedChildEnvNames();
213
+ const probes = [];
214
+ // Source 1: the environment itself. Three checks. The first two are
215
+ // allowlist-INDEPENDENT tripwires (they fire even on a permitted name):
216
+ // no gh-documented token variable, and no git/ssh/node credential- or
217
+ // code-execution-capable variable from the walked vocabulary. The third
218
+ // is the allowlist-dependent stray check (anything outside the
219
+ // constructed contract), which in a real run is tautological because the
220
+ // child is built from that contract, and does real work only in tests
221
+ // that hand-build an env with extra names.
222
+ const names = Object.keys(env).filter((name) => env[name] !== undefined);
223
+ const tokens = names.filter((name) => GH_TOKEN_VARIABLES.includes(name));
224
+ const dangerous = names.filter((name) => isDangerousEnvName(name));
225
+ const strays = names.filter((name) => !permitted.has(name));
226
+ if (tokens.length > 0) {
227
+ probes.push(probe("environment", "resolvable", `pull-request-capable token variable(s) present in the child environment: ${tokens.join(", ")}`));
228
+ }
229
+ else if (dangerous.length > 0) {
230
+ probes.push(probe("environment", "resolvable", `credential- or code-execution-capable variable(s) from the walked vocabulary present in the child environment: ${dangerous.join(", ")}`));
231
+ }
232
+ else if (strays.length > 0) {
233
+ probes.push(probe("environment", "resolvable", `variable(s) outside the constructed contract present in the child environment: ${strays.join(", ")}`));
234
+ }
235
+ else {
236
+ probes.push(probe("environment", "clean", `${String(names.length)} variable(s), all inside the constructed contract, no gh token or walked-vocabulary variable`));
237
+ }
238
+ // Source 2: gh configuration. The store is hosts.yml at gh's documented
239
+ // resolution order (GH_CONFIG_DIR, else XDG_CONFIG_HOME/gh, else
240
+ // HOME/.config/gh); all three are probed rather than only the first so
241
+ // a partial redirection cannot hide a reachable store. Where a gh
242
+ // binary is resolvable on the child PATH, gh itself is also asked: exit
243
+ // 0 from `gh auth status` means some host authenticated, which is a
244
+ // resolvable credential regardless of which file it came from.
245
+ const ghStorePaths = [];
246
+ if (env["GH_CONFIG_DIR"] !== undefined) {
247
+ ghStorePaths.push(join(env["GH_CONFIG_DIR"], "hosts.yml"));
248
+ }
249
+ if (env["XDG_CONFIG_HOME"] !== undefined) {
250
+ ghStorePaths.push(join(env["XDG_CONFIG_HOME"], "gh", "hosts.yml"));
251
+ }
252
+ if (env["HOME"] !== undefined) {
253
+ ghStorePaths.push(join(env["HOME"], ".config", "gh", "hosts.yml"));
254
+ }
255
+ let ghProbe = fileProbe("gh-configuration", ghStorePaths);
256
+ if (ghProbe.outcome === "clean") {
257
+ const gh = findOnPath(env["PATH"], "gh");
258
+ if (gh === undefined) {
259
+ ghProbe = probe("gh-configuration", "clean", `${ghProbe.detail}; no gh binary on the child PATH, so no CLI resolution path exists either`);
260
+ }
261
+ else {
262
+ const status = spawnSync(gh, ["auth", "status"], {
263
+ env: env,
264
+ encoding: "utf8",
265
+ timeout: 15000,
266
+ });
267
+ if (status.error !== undefined) {
268
+ ghProbe = probe("gh-configuration", "error", `gh auth status could not be run: ${singleLine(String(status.error))}`);
269
+ }
270
+ else if (status.status === 0) {
271
+ ghProbe = probe("gh-configuration", "resolvable", "gh auth status exited 0 from inside the child environment: some host is authenticated");
272
+ }
273
+ else {
274
+ ghProbe = probe("gh-configuration", "clean", `${ghProbe.detail}; gh auth status exited ${String(status.status)} (no authenticated host)`);
275
+ }
276
+ }
277
+ }
278
+ probes.push(ghProbe);
279
+ // Sources 3 and 4: git global and system configuration, asked through
280
+ // git itself (`git config --get-all credential.helper`), because git is
281
+ // the program that would resolve a helper and its exit code is the
282
+ // documented contract: 0 with output means the key is set, 1 means it
283
+ // is not. Nothing here parses message text (T-003).
284
+ for (const scope of ["global", "system"]) {
285
+ const result = spawnSync("git", ["config", `--${scope}`, "--get-all", "credential.helper"], {
286
+ env: env,
287
+ encoding: "utf8",
288
+ timeout: 15000,
289
+ });
290
+ const source = `git-${scope}-config`;
291
+ if (result.error !== undefined) {
292
+ probes.push(probe(source, "error", `git config --${scope} could not be run: ${singleLine(String(result.error))}`));
293
+ }
294
+ else if (result.status === 0 && (result.stdout ?? "").trim() !== "") {
295
+ probes.push(probe(source, "resolvable", `git config --${scope} --get-all credential.helper resolves: ${singleLine((result.stdout ?? "").trim())}`));
296
+ }
297
+ else {
298
+ probes.push(probe(source, "clean", `git config --${scope} --get-all credential.helper exited ${String(result.status)} with no output`));
299
+ }
300
+ }
301
+ // Source 5 (behavioral): what git ACTUALLY resolves for credential.helper
302
+ // from inside the child, with NO scope flag. This catches the env-injection
303
+ // vector the two scoped probes structurally miss: a credential.helper
304
+ // injected via git's GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n
305
+ // family resolves here (exit 0 with output) while --global and --system see
306
+ // nothing. It resolves env, then global, then system, then any repo-local
307
+ // config of the working directory. The probe runs from the child's
308
+ // redirected HOME (an empty, non-repo directory) rather than the gate's own
309
+ // cwd, so it does not pick up repo-local config of whatever tree the gate
310
+ // happens to run in; repo-local config of an arbitrary worktree is out of
311
+ // the environment scrub's scope (the gate has no worktree). Nothing here
312
+ // parses message text (T-003): the contract is git's exit code.
313
+ const resolvedCwd = env["HOME"] !== undefined && existsSync(env["HOME"]) ? env["HOME"] : undefined;
314
+ const resolved = spawnSync("git", ["config", "--get-all", "credential.helper"], {
315
+ env: env,
316
+ encoding: "utf8",
317
+ timeout: 15000,
318
+ cwd: resolvedCwd,
319
+ });
320
+ if (resolved.error !== undefined) {
321
+ probes.push(probe("git-resolved-config", "error", `git config --get-all could not be run: ${singleLine(String(resolved.error))}`));
322
+ }
323
+ else if (resolved.status === 0 && (resolved.stdout ?? "").trim() !== "") {
324
+ probes.push(probe("git-resolved-config", "resolvable", `git config --get-all credential.helper resolves from inside the child (any source, including env injection): ${singleLine((resolved.stdout ?? "").trim())}`));
325
+ }
326
+ else {
327
+ probes.push(probe("git-resolved-config", "clean", `git config --get-all credential.helper exited ${String(resolved.status)} with no output`));
328
+ }
329
+ // Source 6: ~/.netrc, resolved from the child HOME.
330
+ const netrcPaths = env["HOME"] === undefined ? [] : [join(env["HOME"], ".netrc")];
331
+ if (netrcPaths.length === 0) {
332
+ probes.push(probe("netrc", "error", "HOME is not set in the child environment, so the netrc resolution path cannot be established"));
333
+ }
334
+ else {
335
+ probes.push(fileProbe("netrc", netrcPaths));
336
+ }
337
+ // Source 7: git's store-backed credential files, both documented
338
+ // locations (~/.git-credentials and $XDG_CONFIG_HOME/git/credentials).
339
+ const credPaths = [];
340
+ if (env["HOME"] !== undefined) {
341
+ credPaths.push(join(env["HOME"], ".git-credentials"));
342
+ }
343
+ if (env["XDG_CONFIG_HOME"] !== undefined) {
344
+ credPaths.push(join(env["XDG_CONFIG_HOME"], "git", "credentials"));
345
+ }
346
+ if (credPaths.length === 0) {
347
+ probes.push(probe("git-credentials", "error", "neither HOME nor XDG_CONFIG_HOME is set in the child environment, so no store path can be established"));
348
+ }
349
+ else {
350
+ probes.push(fileProbe("git-credentials", credPaths));
351
+ }
352
+ return probes;
353
+ }
354
+ /** Fold probes into a gate verdict. Any error wins over any red. */
355
+ export function verdictFromProbes(probes) {
356
+ const errors = probes.filter((entry) => entry.outcome === "error");
357
+ if (errors.length > 0) {
358
+ return {
359
+ status: "error",
360
+ detail: errors.map((entry) => `${entry.source}: ${entry.detail}`).join("; "),
361
+ };
362
+ }
363
+ const resolvable = probes.filter((entry) => entry.outcome === "resolvable");
364
+ if (resolvable.length > 0) {
365
+ return {
366
+ status: "red",
367
+ detail: "credential resolvable from inside the scrubbed child environment: " +
368
+ resolvable.map((entry) => `${entry.source}: ${entry.detail}`).join("; "),
369
+ };
370
+ }
371
+ return {
372
+ status: "green",
373
+ detail: `no pull-request-capable credential resolvable from any of the ${String(probes.length)} probed sources`,
374
+ };
375
+ }
376
+ function parseGateArgs(argv) {
377
+ const [gateId, ...rest] = argv;
378
+ if (gateId !== "credential-scrub" && gateId !== "credential-token") {
379
+ return "expected a gate id: credential-scrub or credential-token";
380
+ }
381
+ let resultPath;
382
+ let evidenceDir;
383
+ for (let i = 0; i < rest.length; i += 1) {
384
+ const flag = rest[i];
385
+ const value = rest[i + 1];
386
+ if (flag === "--result" && value !== undefined) {
387
+ resultPath = value;
388
+ i += 1;
389
+ }
390
+ else if (flag === "--evidence" && value !== undefined) {
391
+ evidenceDir = value;
392
+ i += 1;
393
+ }
394
+ else {
395
+ return `unknown argument ${String(flag)}`;
396
+ }
397
+ }
398
+ if (resultPath === undefined || evidenceDir === undefined) {
399
+ return "both --result <path> and --evidence <dir> are required";
400
+ }
401
+ return {
402
+ gateId,
403
+ resultPath: isAbsolute(resultPath) ? resultPath : resolve(resultPath),
404
+ evidenceDir: isAbsolute(evidenceDir) ? evidenceDir : resolve(evidenceDir),
405
+ };
406
+ }
407
+ function runCredentialScrub(evidenceDir, startedAt) {
408
+ // The gate's harness-owned scrub root lives inside its own evidence
409
+ // directory: the gate probes the CONSTRUCTION, and the construction is
410
+ // the same buildChildEnv the executor calls, so what is green here is
411
+ // the same mechanism spawn hands its children.
412
+ const built = buildChildEnv({
413
+ parentEnv: process.env,
414
+ scrubDir: join(evidenceDir, "scrub-env"),
415
+ });
416
+ if (!built.ok) {
417
+ return {
418
+ result: makeGateResult({
419
+ gate: "credential-scrub",
420
+ status: "error",
421
+ units: 0,
422
+ unitLabel: "credential sources probed",
423
+ startedAt,
424
+ endedAt: new Date().toISOString(),
425
+ detail: `the child environment could not be constructed: ${built.reason}`,
426
+ }),
427
+ };
428
+ }
429
+ const probes = probeCredentialSources(built.env);
430
+ const verdict = verdictFromProbes(probes);
431
+ const units = verdict.status === "error" ? 0 : probes.length;
432
+ return {
433
+ result: makeGateResult({
434
+ gate: "credential-scrub",
435
+ status: verdict.status,
436
+ units,
437
+ unitLabel: "credential sources probed",
438
+ startedAt,
439
+ endedAt: new Date().toISOString(),
440
+ detail: verdict.detail,
441
+ evidence: ["probes.json"],
442
+ }),
443
+ evidenceBody: `${JSON.stringify(probes, null, 2)}\n`,
444
+ };
445
+ }
446
+ function runCredentialToken(startedAt) {
447
+ if (process.env["TIPHYS_IMPLEMENTER_TOKEN"] === undefined) {
448
+ return {
449
+ result: makeGateResult({
450
+ gate: "credential-token",
451
+ status: "not-applicable",
452
+ units: 0,
453
+ unitLabel: "tokens probed",
454
+ startedAt,
455
+ endedAt: new Date().toISOString(),
456
+ detail: "TIPHYS_IMPLEMENTER_TOKEN is not present: owner action A-3 " +
457
+ "(DR-0004 item 4, the scoped implementer token) has not been " +
458
+ "performed, so there is no token to probe. This gate never " +
459
+ "reports green in this state.",
460
+ }),
461
+ };
462
+ }
463
+ // Fail closed (M2-C-3). See the module comment: the safe negative
464
+ // probe's assertion must be derived from captured API responses (plan
465
+ // M2-P8 step 7), and those captures require the very tokens A-3
466
+ // provisions. Guessing a response shape here is T-003 lesson 4.
467
+ return {
468
+ result: makeGateResult({
469
+ gate: "credential-token",
470
+ status: "error",
471
+ units: 0,
472
+ unitLabel: "tokens probed",
473
+ startedAt,
474
+ endedAt: new Date().toISOString(),
475
+ detail: "TIPHYS_IMPLEMENTER_TOKEN is present, but the probe's assertion " +
476
+ "contract has not yet been derived from captured API responses " +
477
+ "(kernel plan M2, M2-P8 step 7; owner action A-3). Refusing to " +
478
+ "assert against an invented response shape (T-003 lesson 4); " +
479
+ "this gate fails closed until the captures exist and is " +
480
+ "witnessed live at the M2 exit test.",
481
+ }),
482
+ };
483
+ }
484
+ function gateMain(argv) {
485
+ const parsed = parseGateArgs(argv);
486
+ if (typeof parsed === "string") {
487
+ process.stderr.write(`credentials gate: ${parsed}\n` +
488
+ "usage: node src/gates/credentials.ts credential-scrub|credential-token " +
489
+ "--result <path> --evidence <dir>\n");
490
+ return EX_USAGE;
491
+ }
492
+ const startedAt = new Date().toISOString();
493
+ const outcome = parsed.gateId === "credential-scrub"
494
+ ? runCredentialScrub(parsed.evidenceDir, startedAt)
495
+ : runCredentialToken(startedAt);
496
+ if (outcome.evidenceBody !== undefined) {
497
+ const evidencePath = join(parsed.evidenceDir, "probes.json");
498
+ const refusal = refuseOpenForWrite(evidencePath);
499
+ if (refusal !== undefined) {
500
+ process.stderr.write(`credentials gate: ${refusal}\n`);
501
+ return exitCodeForStatus("error");
502
+ }
503
+ const wrote = runStep(`writing ${evidencePath}`, () => {
504
+ writeFileSync(evidencePath, outcome.evidenceBody);
505
+ });
506
+ if (!wrote.ok) {
507
+ process.stderr.write(`credentials gate: ${wrote.reason}\n`);
508
+ return exitCodeForStatus("error");
509
+ }
510
+ }
511
+ const recordRefusal = refuseOpenForWrite(parsed.resultPath);
512
+ if (recordRefusal !== undefined) {
513
+ process.stderr.write(`credentials gate: ${recordRefusal}\n`);
514
+ return exitCodeForStatus("error");
515
+ }
516
+ const written = runStep(`writing ${parsed.resultPath}`, () => {
517
+ writeFileSync(parsed.resultPath, renderGateResult(outcome.result));
518
+ });
519
+ if (!written.ok) {
520
+ process.stderr.write(`credentials gate: ${written.reason}\n`);
521
+ return exitCodeForStatus("error");
522
+ }
523
+ return exitCodeForStatus(outcome.result.status);
524
+ }
525
+ // Main guard: run as a gate subprocess when executed directly, inert on
526
+ // import (tests import the probe functions without running a gate).
527
+ const entry = process.argv[1];
528
+ if (entry !== undefined) {
529
+ const isMain = pathsIdentifySameObject(fileURLToPath(import.meta.url), entry);
530
+ if (isMain) {
531
+ process.exit(gateMain(process.argv.slice(2)));
532
+ }
533
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { runReleaseGate } from "./release.js";
4
+ /**
5
+ * THE DEPLOY GATE ENTRY (kernel plan M2, M2-P7 step 4). A thin entry point
6
+ * that names the declared `deploy` verification and calls the one contract
7
+ * module, `src/gates/release.ts`. The manifest keeps its two static entries
8
+ * exactly as section 1.4 declares them, so no gate-runner or exit-test
9
+ * arithmetic changes, and M2 deliberately does not generalize to N
10
+ * charter-declared verifications (section 2 item 12; that is M3's).
11
+ *
12
+ * Release verification is structurally NOT a pull-request gate: it runs
13
+ * after a merge, against a commit that exists only once the merge has
14
+ * happened, so a pre-merge bundle can only ever report not-applicable, on
15
+ * every repository (investigation section 1.3, observation O-3). The real
16
+ * call site is the orchestrator's post-merge step with the merged sha as
17
+ * `--base`, deferred to M4 by M2-D-11.
18
+ */
19
+ function isMain() {
20
+ const argv1 = process.argv[1];
21
+ if (argv1 === undefined) {
22
+ return false;
23
+ }
24
+ try {
25
+ return realpathSync(argv1) === realpathSync(fileURLToPath(import.meta.url));
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ if (isMain()) {
32
+ process.exitCode = await runReleaseGate("deploy", process.argv.slice(2));
33
+ }