@opengsd/gsd-core 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -78,6 +78,19 @@
|
|
|
78
78
|
// scans a `<( )` operand; and `find … | xargs cat` infers the upstream
|
|
79
79
|
// segment's names as the sub-command's read operands.
|
|
80
80
|
//
|
|
81
|
+
// Project exemptions (#4432): `hooks.secret_read_exempt` in the root project's
|
|
82
|
+
// .planning/config.json lists repo-relative files that stay readable. An entry
|
|
83
|
+
// takes effect only for a file that is a regular file on disk AND a regular
|
|
84
|
+
// blob (100644/100755) in the commit HEAD names — read from `.git` directly,
|
|
85
|
+
// no subprocess, no timer (hooks/lib/git-head-tree.js). Read and Grep honor an
|
|
86
|
+
// absolute or cwd-relative path; Bash honors the absolute path only, and only
|
|
87
|
+
// when EVERY command on the line is one of cat/head/tail/grep/egrep/fgrep/rg/
|
|
88
|
+
// wc/diff/cmp/sort/uniq/cut/nl/od (after wrapper resolution) with no `$( )`/
|
|
89
|
+
// backtick/`<( )` body, heredoc or file-writing redirect or option; any other
|
|
90
|
+
// command (mv, ln, cp, git, sh, awk, sed, …) withdraws it for the whole line. A Grep
|
|
91
|
+
// `glob` is never widened, and a colon-joined word (`HEAD:.env.production`)
|
|
92
|
+
// never matches. An unreadable config or repository means no exemption.
|
|
93
|
+
//
|
|
81
94
|
// Grep globs are judged per brace alternative (never on the whole glob, so
|
|
82
95
|
// `{.env.local,zzz.ts}` cannot hide behind a benign sibling): a pure-wildcard
|
|
83
96
|
// alternative (`*`, `**`) is allowed — Grep already skips gitignored files,
|
|
@@ -107,7 +120,16 @@
|
|
|
107
120
|
// destination exemption can move a link prepared under a non-secret name
|
|
108
121
|
// onto a secret name (`ln -s .env.example l && mv l .env`), so a later
|
|
109
122
|
// edit of `.env` lands in the link target — a write redirection, the
|
|
110
|
-
// out-of-scope write class, not a read.
|
|
123
|
+
// out-of-scope write class, not a read. #4432 adds three stated limits to
|
|
124
|
+
// the project exemptions: (1) an agent with Bash can rewrite local git state
|
|
125
|
+
// (force-add + commit, `update-ref`) to make a file committed; the
|
|
126
|
+
// committed-file rule stops the one-step "edit the config, then read" path
|
|
127
|
+
// and is the same class as the interpreter one-liner gap above — a
|
|
128
|
+
// guardrail, not a sandbox. (2) The guard vouches for the path, not for
|
|
129
|
+
// working-tree bytes that differ from HEAD; a content-hash check would block
|
|
130
|
+
// re-reading the template after each edit, the flow the issue asks for.
|
|
131
|
+
// (3) A file inside a git submodule cannot be exempted: the submodule's
|
|
132
|
+
// `.git` is a file the reader does not follow.)
|
|
111
133
|
//
|
|
112
134
|
// A second, unrelated contract is enforced on the Bash leg because this is the
|
|
113
135
|
// one hook that already parses EVERY Bash command (#5214): a Bash command that
|
|
@@ -148,6 +170,95 @@ const MAX_NESTING_DEPTH = 3;
|
|
|
148
170
|
// Brace-alternative budget for a Grep glob before it is denied as too complex.
|
|
149
171
|
const MAX_GLOB_ALTERNATIVES = 64;
|
|
150
172
|
|
|
173
|
+
// #4432: project exemptions (`hooks.secret_read_exempt` in the ROOT project's
|
|
174
|
+
// .planning/config.json). `root`/`allowRelative` are set once per invocation by
|
|
175
|
+
// the stdin handler; `checker` is built lazily on the first secret-classified
|
|
176
|
+
// token. Any failure while building or calling it means NOT exempt — never an
|
|
177
|
+
// allow, and never a throw into the fail-open crash path.
|
|
178
|
+
const exemption = { root: null, allowRelative: false, checker: null, configured: false, bashLine: null, bashReadOnly: null };
|
|
179
|
+
|
|
180
|
+
// F3: a Bash exemption holds only when every command on the line is one of
|
|
181
|
+
// these read-only readers; any other command anywhere on the line withdraws it
|
|
182
|
+
// for every token (`mv a b && cat <exempt>` re-anchors or launders the name).
|
|
183
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
184
|
+
'cat', 'head', 'tail', 'grep', 'egrep', 'fgrep', 'rg', 'wc', 'diff', 'cmp', 'sort', 'uniq', 'cut', 'nl', 'od',
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
// Per-command options that write a file or run a program, so the command is
|
|
188
|
+
// not read-only: `sort -o f`, `sort --compress-program`, `rg --pre`.
|
|
189
|
+
function readOnlyOptionsOnly(base, operands) {
|
|
190
|
+
let positional = 0;
|
|
191
|
+
for (const w of operands) {
|
|
192
|
+
const t = w.text;
|
|
193
|
+
if (base === 'sort' && (/^-[A-Za-z]*o/.test(t) || /^--(output|compress-program|files0-from)/.test(t))) return false;
|
|
194
|
+
if (base === 'rg' && /^--(pre|pre-glob|hostname-bin)(=|$)/.test(t)) return false;
|
|
195
|
+
if (base === 'uniq' && !t.startsWith('-')) positional++;
|
|
196
|
+
}
|
|
197
|
+
return base !== 'uniq' || positional <= 1; // `uniq in out` writes `out`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Fail closed: a substitution body, heredoc, process substitution, file
|
|
201
|
+
// redirect out, unresolved or unlisted command word, or any exception is false.
|
|
202
|
+
function lineIsReadOnly(command) {
|
|
203
|
+
try {
|
|
204
|
+
const { tokens, nested } = tokenize(command);
|
|
205
|
+
if (nested.length > 0) return false;
|
|
206
|
+
const bySeg = new Map();
|
|
207
|
+
for (const t of tokens) {
|
|
208
|
+
if (t.kind === 'heredoc') return false;
|
|
209
|
+
if (t.kind === 'sep') continue;
|
|
210
|
+
if (t.kind === 'word' && t.procsub !== undefined) return false;
|
|
211
|
+
if (!bySeg.has(t.seg)) bySeg.set(t.seg, []);
|
|
212
|
+
bySeg.get(t.seg).push(t);
|
|
213
|
+
}
|
|
214
|
+
for (const segTokens of bySeg.values()) {
|
|
215
|
+
const words = [];
|
|
216
|
+
for (let k = 0; k < segTokens.length; k++) {
|
|
217
|
+
const t = segTokens[k];
|
|
218
|
+
if (t.kind === 'op') {
|
|
219
|
+
if (t.dup) continue;
|
|
220
|
+
const target = segTokens[k + 1];
|
|
221
|
+
if (!target || target.kind !== 'word') return false;
|
|
222
|
+
k++;
|
|
223
|
+
if (!t.read && target.text !== '/dev/null') return false; // output to a file
|
|
224
|
+
if (t.text.endsWith('<<<')) return false;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
words.push(t);
|
|
228
|
+
}
|
|
229
|
+
if (words.length === 0) return false;
|
|
230
|
+
const cmd = resolveCommand(words);
|
|
231
|
+
if (!cmd || !READ_ONLY_COMMANDS.has(cmd.base)) return false;
|
|
232
|
+
if (!readOnlyOptionsOnly(cmd.base, cmd.operands)) return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isExemptSecret(tok) {
|
|
241
|
+
if (exemption.root === null) return false;
|
|
242
|
+
if (exemption.bashLine !== null) {
|
|
243
|
+
if (exemption.bashReadOnly === null) exemption.bashReadOnly = lineIsReadOnly(exemption.bashLine);
|
|
244
|
+
if (exemption.bashReadOnly !== true) return false;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
if (exemption.checker === null) {
|
|
248
|
+
const lib = require('./lib/secret-read-exemptions.js');
|
|
249
|
+
const entries = lib.readExemptEntries(exemption.root);
|
|
250
|
+
exemption.configured = entries.length > 0;
|
|
251
|
+
exemption.checker = entries.length === 0
|
|
252
|
+
? () => false
|
|
253
|
+
: lib.createExemptionChecker({ root: exemption.root, entries, allowRelative: exemption.allowRelative });
|
|
254
|
+
}
|
|
255
|
+
return exemption.checker(tok) === true;
|
|
256
|
+
} catch {
|
|
257
|
+
exemption.checker = () => false;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
151
262
|
// `.env.<suffix>` names that are templates, not secrets (case-insensitive).
|
|
152
263
|
const NON_SECRET_ENV_SUFFIXES = new Set(['example', 'sample', 'template', 'dist']);
|
|
153
264
|
|
|
@@ -163,8 +274,11 @@ const NON_SECRET_ENV_SUFFIXES = new Set(['example', 'sample', 'template', 'dist'
|
|
|
163
274
|
// stdbuf(1), setsid(1) and bash's `command`/`exec`. A wrong entry fails open
|
|
164
275
|
// (the option's value becomes the command word), so keep it to the man pages.
|
|
165
276
|
const WRAPPERS = new Map([
|
|
166
|
-
['sudo', { short: '
|
|
167
|
-
|
|
277
|
+
['sudo', { short: 'aCcDghpRrTtUu', long: ['--auth-type', '--close-from', '--login-class', '--chdir', '--group', '--host', '--prompt', '--chroot', '--role', '--command-timeout', '--type', '--other-user', '--user'],
|
|
278
|
+
// No-value long options, so an exact one never prefix-matches a longer
|
|
279
|
+
// valued one (`--login` vs `--login-class`).
|
|
280
|
+
flags: ['--askpass', '--background', '--bell', '--edit', '--help', '--list', '--login', '--non-interactive', '--preserve-env', '--preserve-groups', '--remove-timestamp', '--reset-timestamp', '--set-home', '--shell', '--stdin', '--validate', '--version'] }],
|
|
281
|
+
['doas', { short: 'aCu' }],
|
|
168
282
|
['env', { short: 'uCSaP', long: ['--unset', '--chdir', '--split-string', '--argv0'], assigns: true }],
|
|
169
283
|
['nice', { short: 'n', long: ['--adjustment'] }],
|
|
170
284
|
['nohup', {}],
|
|
@@ -309,7 +423,7 @@ function isSecretBasename(name) {
|
|
|
309
423
|
// lower case once at the top so `.ENV` / `.Secrets` match on the
|
|
310
424
|
// case-insensitive filesystems (macOS, Windows) where they ARE the secret file
|
|
311
425
|
// — the same stance as the write guard's `/i` patterns.
|
|
312
|
-
function
|
|
426
|
+
function namesSecretByName(tok) {
|
|
313
427
|
if (typeof tok !== 'string' || tok === '') return false;
|
|
314
428
|
const lower = tok.toLowerCase();
|
|
315
429
|
if (isSecretBasename(lastSegment(lower))) return true;
|
|
@@ -317,6 +431,15 @@ function namesSecret(tok) {
|
|
|
317
431
|
return colon !== -1 && isSecretBasename(lastSegment(lower.slice(colon + 1)));
|
|
318
432
|
}
|
|
319
433
|
|
|
434
|
+
// A secret-by-name token is still allowed when the project listed that exact
|
|
435
|
+
// committed file under `hooks.secret_read_exempt` (#4432). The exemption is
|
|
436
|
+
// consulted only on the secret-classified branch, so an ordinary token costs
|
|
437
|
+
// no I/O.
|
|
438
|
+
function namesSecret(tok) {
|
|
439
|
+
if (!namesSecretByName(tok)) return false;
|
|
440
|
+
return !isExemptSecret(tok);
|
|
441
|
+
}
|
|
442
|
+
|
|
320
443
|
// ---------------------------------------------------------------------------
|
|
321
444
|
// Grep glob analysis
|
|
322
445
|
// ---------------------------------------------------------------------------
|
|
@@ -886,7 +1009,9 @@ const GROUPING_SEPS = new Set(['(', ')', '{', '}']);
|
|
|
886
1009
|
function wrapperOptionWidth(word, spec) {
|
|
887
1010
|
if (word.startsWith('--')) {
|
|
888
1011
|
if (word.includes('=')) return 1;
|
|
889
|
-
// getopt_long
|
|
1012
|
+
// getopt_long takes an exact match over any longer option it prefixes
|
|
1013
|
+
// (`sudo --login` is not `--login-class`), else any unambiguous prefix (`--adj 5`).
|
|
1014
|
+
if ((spec.flags || []).includes(word)) return 1;
|
|
890
1015
|
return word.length > 2 && (spec.long || []).some((l) => l.startsWith(word)) ? 2 : 1;
|
|
891
1016
|
}
|
|
892
1017
|
const valued = spec.short || '';
|
|
@@ -1410,7 +1535,10 @@ function reasonFor(code, tool, target) {
|
|
|
1410
1535
|
return `Secret read guard: ${tool} would read '${target}', which matches a protected secret-file ` +
|
|
1411
1536
|
`pattern (${PATTERN_TEXT}). Secret values must not be read into the conversation. ` +
|
|
1412
1537
|
'If you need a specific value, ask the user for it; if you need the variable NAMES, ' +
|
|
1413
|
-
'read the non-secret template (.env.example) instead.'
|
|
1538
|
+
'read the non-secret template (.env.example) instead.' +
|
|
1539
|
+
(tool === 'Bash' && exemption.configured
|
|
1540
|
+
? ' Files the project lists under hooks.secret_read_exempt are readable from Bash only by their absolute path.'
|
|
1541
|
+
: '');
|
|
1414
1542
|
}
|
|
1415
1543
|
|
|
1416
1544
|
// stdout gets the typed JSON block; stderr gets the plain reason string
|
|
@@ -1532,6 +1660,12 @@ process.stdin.on('end', () => {
|
|
|
1532
1660
|
allow(undefined);
|
|
1533
1661
|
}
|
|
1534
1662
|
|
|
1663
|
+
// #4432: exemptions resolve against the payload cwd (as the context
|
|
1664
|
+
// monitor does), else the process cwd. Bash honors absolute paths only: a
|
|
1665
|
+
// relative word there is re-anchored by cd/find/xargs/git -C.
|
|
1666
|
+
exemption.root = typeof data.cwd === 'string' && data.cwd !== '' ? data.cwd : process.cwd();
|
|
1667
|
+
exemption.allowRelative = tool === 'Read' || tool === 'Grep';
|
|
1668
|
+
|
|
1535
1669
|
// Every payload field is read TYPED in a single statement (#2547 class):
|
|
1536
1670
|
// `[]`/`{}` are truthy and a non-string degrades to '' here.
|
|
1537
1671
|
if (tool === 'Read') {
|
|
@@ -1556,6 +1690,7 @@ process.stdin.on('end', () => {
|
|
|
1556
1690
|
const command = typeof data.tool_input.command === 'string' ? data.tool_input.command : '';
|
|
1557
1691
|
if (command === '') allow(undefined);
|
|
1558
1692
|
if (command.length > MAX_COMMAND_LENGTH) emitBlock('command-too-large', tool, '');
|
|
1693
|
+
exemption.bashLine = command;
|
|
1559
1694
|
const hit = findSecretRead(command, 0) || findSecretReadUnderOtherReadings(command);
|
|
1560
1695
|
if (hit !== null) emitBlock('secret-read', tool, hit);
|
|
1561
1696
|
const spawn = findDebuggerCliSpawn(command, 0);
|
|
@@ -18,6 +18,7 @@ const os = require('os');
|
|
|
18
18
|
// monkeypatching childProcess.execFileSync.
|
|
19
19
|
const childProcess = require('child_process');
|
|
20
20
|
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
21
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
21
22
|
const { gitEnv } = require('./lib/git-probe.js');
|
|
22
23
|
|
|
23
24
|
// This hook's build-seam outer catch (require.main guard just below) has
|
|
@@ -81,7 +82,7 @@ function readGsdConfig(dir) {
|
|
|
81
82
|
const candidate = path.join(current, '.planning', 'config.json');
|
|
82
83
|
if (fs.existsSync(candidate)) {
|
|
83
84
|
try {
|
|
84
|
-
return JSON.parse(
|
|
85
|
+
return JSON.parse(readConfigFileBounded(candidate, { required: true })) || {};
|
|
85
86
|
} catch (e) {
|
|
86
87
|
return {};
|
|
87
88
|
}
|
|
@@ -254,7 +255,7 @@ const SETTINGS_FILE_MAX_BYTES = 1024 * 1024;
|
|
|
254
255
|
function readSettingsFile(file) {
|
|
255
256
|
let fd;
|
|
256
257
|
try {
|
|
257
|
-
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0));
|
|
258
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0) | (fs.constants.O_NOCTTY || 0));
|
|
258
259
|
const st = fs.fstatSync(fd);
|
|
259
260
|
if (!st.isFile() || st.size > SETTINGS_FILE_MAX_BYTES) return null;
|
|
260
261
|
// One byte past the checked size, so growth after the check shows as a longer read.
|
|
@@ -22,6 +22,7 @@ const path = require('path');
|
|
|
22
22
|
const { spawnSync } = require('child_process');
|
|
23
23
|
const { tokenize, skipToSubcommand } = require('./lib/git-cmd.js');
|
|
24
24
|
const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
|
|
25
|
+
const { readConfigFileBounded } = require('./lib/config-read.js');
|
|
25
26
|
const { reportIfUndetermined, gitEnv, BLOCKING_GUARD_PROBE_TIMEOUT_MS } = require('./lib/git-probe.js');
|
|
26
27
|
|
|
27
28
|
// This guard is almost entirely advisory (fail open — a broken advisory must
|
|
@@ -179,7 +180,7 @@ function workflowGuardEnabled(cwd) {
|
|
|
179
180
|
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
180
181
|
if (!fs.existsSync(configPath)) return false;
|
|
181
182
|
try {
|
|
182
|
-
const config = JSON.parse(
|
|
183
|
+
const config = JSON.parse(readConfigFileBounded(configPath, { required: true }));
|
|
183
184
|
return Boolean(config.hooks?.workflow_guard);
|
|
184
185
|
} catch (e) {
|
|
185
186
|
return false;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// hooks/lib/config-read.js — the one bounded reader for a project config.json used by every
|
|
3
|
+
// hook (#5759). Hooks are standalone files staged at install, so this mirrors the built
|
|
4
|
+
// gsd-core/bin/lib/config-file-read.cjs (a test pins that the two caps agree) instead of
|
|
5
|
+
// requiring it.
|
|
6
|
+
//
|
|
7
|
+
// A config.json planted as a FIFO or device blocks a plain readFileSync forever, and a hook
|
|
8
|
+
// runs inside the user's session on every tool call. The file is opened non-blocking and
|
|
9
|
+
// checked through its own descriptor (O_NONBLOCK is absent on Windows, hence `|| 0`).
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
|
|
12
|
+
const CONFIG_MAX_BYTES = 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
function coded(message, code) {
|
|
15
|
+
return Object.assign(new Error(message), { code });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The UTF-8 text of `file`. A file that is absent returns null, or — with `{ required: true }` —
|
|
20
|
+
* throws an error with code ENOENT, so a caller that already treats any throw as "no usable
|
|
21
|
+
* config" keeps its exact semantics. A FIFO/device (ENXIO), directory (EISDIR), file over
|
|
22
|
+
* CONFIG_MAX_BYTES (EFBIG) or any other failure always throws with `.code` set.
|
|
23
|
+
*/
|
|
24
|
+
function readConfigFileBounded(file, options) {
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0) | (fs.constants.O_NOCTTY || 0));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err && err.code === 'ENOENT' && !(options && options.required)) return null;
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const st = fs.fstatSync(fd);
|
|
34
|
+
if (!st.isFile()) throw coded(`${file} is not a regular file`, st.isDirectory() ? 'EISDIR' : 'ENXIO');
|
|
35
|
+
if (st.size > CONFIG_MAX_BYTES) throw coded(`${file} exceeds ${CONFIG_MAX_BYTES} bytes`, 'EFBIG');
|
|
36
|
+
const buf = Buffer.allocUnsafe(st.size);
|
|
37
|
+
let off = 0;
|
|
38
|
+
while (off < st.size) {
|
|
39
|
+
const n = fs.readSync(fd, buf, off, st.size - off, off);
|
|
40
|
+
if (n <= 0) break;
|
|
41
|
+
off += n;
|
|
42
|
+
}
|
|
43
|
+
return buf.toString('utf8', 0, off);
|
|
44
|
+
} finally {
|
|
45
|
+
try { fs.closeSync(fd); } catch (e) { /* already closed */ }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { CONFIG_MAX_BYTES, readConfigFileBounded };
|
|
@@ -49,6 +49,8 @@ const ARGUMENT_TAKING_FLAGS = new Set([
|
|
|
49
49
|
'--man-path',
|
|
50
50
|
'--info-path',
|
|
51
51
|
'--list-cmds',
|
|
52
|
+
'--config-env', // config from an env var: `--config-env=k=V` or `--config-env k=V` (#5821)
|
|
53
|
+
'--attr-source', // tree-ish for attributes: `--attr-source=T` or `--attr-source T` (#5821)
|
|
52
54
|
]);
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -59,7 +61,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
59
61
|
'--no-replace-objects', '--bare',
|
|
60
62
|
'--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs',
|
|
61
63
|
'--icase-pathspecs', '--no-optional-locks',
|
|
62
|
-
'-P', '--no-lazy-fetch',
|
|
64
|
+
'-P', '--no-lazy-fetch', '--no-advice',
|
|
63
65
|
'--version', '--help',
|
|
64
66
|
]);
|
|
65
67
|
|