@deftai/directive-core 0.94.0 → 0.95.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/dist/authz/classify.js +485 -0
- package/dist/doctor/main.js +41 -9
- package/dist/doctor/types.d.ts +2 -2
- package/dist/freshness/bind.d.ts +67 -0
- package/dist/freshness/bind.js +201 -0
- package/dist/freshness/cli.d.ts +30 -0
- package/dist/freshness/cli.js +180 -0
- package/dist/freshness/compare.d.ts +14 -0
- package/dist/freshness/compare.js +134 -0
- package/dist/freshness/generation.d.ts +44 -0
- package/dist/freshness/generation.js +172 -0
- package/dist/freshness/index.d.ts +13 -0
- package/dist/freshness/index.js +13 -0
- package/dist/freshness/report.d.ts +43 -0
- package/dist/freshness/report.js +139 -0
- package/dist/freshness/types.d.ts +70 -0
- package/dist/freshness/types.js +30 -0
- package/dist/handoff-evidence/index.d.ts +8 -0
- package/dist/handoff-evidence/index.js +7 -0
- package/dist/handoff-evidence/validate.d.ts +105 -0
- package/dist/handoff-evidence/validate.js +423 -0
- package/dist/hooks/dispatcher.d.ts +3 -0
- package/dist/hooks/dispatcher.js +12 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +5 -2
- package/dist/init-deposit/init-deposit.d.ts +7 -1
- package/dist/init-deposit/init-deposit.js +24 -4
- package/dist/init-deposit/refresh.d.ts +9 -1
- package/dist/init-deposit/refresh.js +48 -5
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +35 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
- package/dist/session/ritual-entrypoint.d.ts +6 -2
- package/dist/session/ritual-entrypoint.js +15 -2
- package/dist/session/session-ready.js +31 -14
- package/dist/session/session-start.d.ts +2 -1
- package/dist/session/session-start.js +66 -3
- package/dist/session/verify-session-ritual.d.ts +4 -2
- package/dist/session/verify-session-ritual.js +11 -11
- package/dist/slash/product-set.js +4 -1
- package/dist/swarm/verify-review-clean.d.ts +4 -3
- package/dist/swarm/verify-review-clean.js +28 -73
- package/dist/triage/classify/index.d.ts +9 -0
- package/dist/triage/classify/index.js +34 -2
- package/dist/triage/classify/label-mirror.d.ts +143 -0
- package/dist/triage/classify/label-mirror.js +684 -0
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +27 -7
- package/dist/triage/queue/show.d.ts +1 -1
- package/dist/triage/queue/show.js +4 -2
- package/dist/vbrief-validate/project-definition.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
- package/dist/verify-env/agent-hook-readiness.js +150 -0
- package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
- package/dist/verify-env/agent-hooks-live-probe.js +120 -107
- package/dist/verify-env/agent-hooks.js +11 -3
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/package.json +7 -3
package/dist/authz/classify.js
CHANGED
|
@@ -145,6 +145,442 @@ function hasGhApiPath(tokens, needle) {
|
|
|
145
145
|
}
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Authz authority-mutating CLI verbs (#3110). Classified as **settings** so under
|
|
150
|
+
* active UAT they deny without a prior human grant — never empty → shell-op-unclassifiable fail-open.
|
|
151
|
+
*/
|
|
152
|
+
const AUTHZ_MUTATING_SUBCOMMANDS = new Set(["grant", "uat-start", "uat-suspend", "revoke"]);
|
|
153
|
+
function authzSubcommandFromToken(token) {
|
|
154
|
+
const t = normalizeToken(token);
|
|
155
|
+
if (t.startsWith("authz:")) {
|
|
156
|
+
const sub = t.slice("authz:".length);
|
|
157
|
+
return AUTHZ_MUTATING_SUBCOMMANDS.has(sub) ? sub : null;
|
|
158
|
+
}
|
|
159
|
+
return AUTHZ_MUTATING_SUBCOMMANDS.has(t) ? t : null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Detect `deft|task|directive authz:grant` / `authz grant` (and wrappers) in shell tokens.
|
|
163
|
+
* O(n) token walk — no nested-quantifier regex on untrusted input.
|
|
164
|
+
*/
|
|
165
|
+
function hasAuthzMutatingCli(tokens) {
|
|
166
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
167
|
+
const raw = tokens[i];
|
|
168
|
+
if (raw === undefined)
|
|
169
|
+
break;
|
|
170
|
+
const t = normalizeToken(raw);
|
|
171
|
+
// Combined form anywhere: authz:grant / authz:uat-suspend / …
|
|
172
|
+
if (authzSubcommandFromToken(t) !== null && t.startsWith("authz:")) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
// Separated form: … authz grant|uat-start|uat-suspend|revoke
|
|
176
|
+
// Also path-ish bins ending in /authz or \authz (node …/authz.js grant).
|
|
177
|
+
const isAuthzBin = t === "authz" ||
|
|
178
|
+
t.endsWith("/authz") ||
|
|
179
|
+
t.endsWith("\\authz") ||
|
|
180
|
+
t.endsWith("/authz.js") ||
|
|
181
|
+
t.endsWith("\\authz.js") ||
|
|
182
|
+
t.endsWith("/authz.ts") ||
|
|
183
|
+
t.endsWith("\\authz.ts");
|
|
184
|
+
if (!isAuthzBin)
|
|
185
|
+
continue;
|
|
186
|
+
const next = tokens[i + 1] !== undefined ? normalizeToken(tokens[i + 1]) : "";
|
|
187
|
+
if (authzSubcommandFromToken(next) !== null)
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Path-ish normalize: keep separators (do not strip `\` like normalizeToken).
|
|
194
|
+
*/
|
|
195
|
+
function pathishToken(token) {
|
|
196
|
+
return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Shell **write** targeting `.deft/authz/` (#3110 AC-3).
|
|
200
|
+
* Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
|
|
201
|
+
* Redirects only count when the destination region contains `.deft/authz`.
|
|
202
|
+
*/
|
|
203
|
+
function hasAuthzDirShellWrite(command, tokens) {
|
|
204
|
+
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
205
|
+
if (!lower.includes(".deft/authz"))
|
|
206
|
+
return false;
|
|
207
|
+
// Redirect dest region after each `>` / `>>` (O(n); no nested-quantifier regex).
|
|
208
|
+
for (let i = 0; i < lower.length; i++) {
|
|
209
|
+
if (lower[i] !== ">")
|
|
210
|
+
continue;
|
|
211
|
+
let j = i + 1;
|
|
212
|
+
if (j < lower.length && lower[j] === ">")
|
|
213
|
+
j++;
|
|
214
|
+
// Dest until pipe/semicolon/ampersand/newline.
|
|
215
|
+
let end = j;
|
|
216
|
+
while (end < lower.length &&
|
|
217
|
+
lower[end] !== "|" &&
|
|
218
|
+
lower[end] !== ";" &&
|
|
219
|
+
lower[end] !== "&" &&
|
|
220
|
+
lower[end] !== "\n") {
|
|
221
|
+
end++;
|
|
222
|
+
}
|
|
223
|
+
if (lower.slice(j, end).includes(".deft/authz"))
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// Write/destructive bins with an authz path argument.
|
|
227
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
228
|
+
if (!INDIRECT_WRITE_BINS.has(normalizeToken(tokens[ti])))
|
|
229
|
+
continue;
|
|
230
|
+
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
231
|
+
if (pathishToken(tokens[tj]).includes(".deft/authz"))
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
/** Write/destructive shell bins (token match after normalizeToken). */
|
|
238
|
+
const INDIRECT_WRITE_BINS = new Set([
|
|
239
|
+
"dd",
|
|
240
|
+
"sed",
|
|
241
|
+
"tee",
|
|
242
|
+
"cp",
|
|
243
|
+
"mv",
|
|
244
|
+
"rsync",
|
|
245
|
+
"rm",
|
|
246
|
+
"rmdir",
|
|
247
|
+
"unlink",
|
|
248
|
+
"shred",
|
|
249
|
+
"truncate",
|
|
250
|
+
"chmod",
|
|
251
|
+
"chown",
|
|
252
|
+
"install",
|
|
253
|
+
"python",
|
|
254
|
+
"python3",
|
|
255
|
+
"node",
|
|
256
|
+
"perl",
|
|
257
|
+
"ruby",
|
|
258
|
+
"pwsh",
|
|
259
|
+
"powershell",
|
|
260
|
+
"set-content",
|
|
261
|
+
"out-file",
|
|
262
|
+
"add-content",
|
|
263
|
+
"copy-item",
|
|
264
|
+
"move-item",
|
|
265
|
+
"remove-item",
|
|
266
|
+
"ri",
|
|
267
|
+
"ni",
|
|
268
|
+
"sc",
|
|
269
|
+
"mi",
|
|
270
|
+
]);
|
|
271
|
+
/**
|
|
272
|
+
* O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
|
|
273
|
+
* (no nested-quantifier regex). Includes command substitution and positional `$1`.
|
|
274
|
+
*/
|
|
275
|
+
function hasEnvExpansion(command) {
|
|
276
|
+
for (let i = 0; i < command.length; i++) {
|
|
277
|
+
const c = command[i];
|
|
278
|
+
if (c === "`")
|
|
279
|
+
return true;
|
|
280
|
+
if (c === "$" && i + 1 < command.length) {
|
|
281
|
+
const n = command[i + 1];
|
|
282
|
+
// $VAR / ${VAR} / $(cmd) / $1 / $@ / $* / $? / $'…' (ANSI-C)
|
|
283
|
+
if (n === "{" ||
|
|
284
|
+
n === "(" ||
|
|
285
|
+
n === "_" ||
|
|
286
|
+
n === "'" ||
|
|
287
|
+
n === "@" ||
|
|
288
|
+
n === "*" ||
|
|
289
|
+
n === "?" ||
|
|
290
|
+
n === "#" ||
|
|
291
|
+
n === "!" ||
|
|
292
|
+
(n >= "0" && n <= "9") ||
|
|
293
|
+
(n >= "A" && n <= "Z") ||
|
|
294
|
+
(n >= "a" && n <= "z")) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (c === "%" && i + 1 < command.length) {
|
|
299
|
+
const n = command[i + 1];
|
|
300
|
+
if (n === "_" || (n >= "A" && n <= "Z") || (n >= "a" && n <= "z")) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
function hasWriteShape(command, tokens) {
|
|
308
|
+
if (command.includes(">"))
|
|
309
|
+
return true;
|
|
310
|
+
for (const t of tokens) {
|
|
311
|
+
if (INDIRECT_WRITE_BINS.has(normalizeToken(t)))
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Split-path containment: `.deft` and `authz` both appear (e.g. `cd .deft && … authz/…`).
|
|
318
|
+
* O(n) substring checks — no nested-quantifier regex.
|
|
319
|
+
*/
|
|
320
|
+
function hasSplitAuthzPath(command) {
|
|
321
|
+
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
322
|
+
if (!lower.includes("authz"))
|
|
323
|
+
return false;
|
|
324
|
+
return lower.includes(".deft") || lower.includes("/deft/") || lower.includes("deft/");
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Last non-flag token is a pure expansion dest (`$STORE`, `${STORE}`, `%TEMP%`)
|
|
328
|
+
* with no trailing path segment (`$HOME/out` is NOT pure — ordinary user write).
|
|
329
|
+
*/
|
|
330
|
+
function lastTokenIsOpaqueExpansion(tokens) {
|
|
331
|
+
let last = "";
|
|
332
|
+
for (const t of tokens) {
|
|
333
|
+
if (t.startsWith("-"))
|
|
334
|
+
continue;
|
|
335
|
+
last = t;
|
|
336
|
+
}
|
|
337
|
+
if (last.length === 0)
|
|
338
|
+
return false;
|
|
339
|
+
const n = last.replace(/['"]/g, "");
|
|
340
|
+
// Path after expansion → ordinary dest, not opaque store alias.
|
|
341
|
+
if (n.includes("/") || n.includes("\\"))
|
|
342
|
+
return false;
|
|
343
|
+
if (n.startsWith("$") && n.length > 1)
|
|
344
|
+
return true;
|
|
345
|
+
if (n.startsWith("%") && n.endsWith("%") && n.length > 2)
|
|
346
|
+
return true;
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
/** Env / path tokens that are ordinary non-store destinations (not authz containment). */
|
|
350
|
+
const ORDINARY_EXPANSION_PREFIXES = [
|
|
351
|
+
"home",
|
|
352
|
+
"tmpdir",
|
|
353
|
+
"temp",
|
|
354
|
+
"tmp",
|
|
355
|
+
"pwd",
|
|
356
|
+
"user",
|
|
357
|
+
"username",
|
|
358
|
+
"userprofile",
|
|
359
|
+
"xdg_",
|
|
360
|
+
"path",
|
|
361
|
+
"psmodulepath",
|
|
362
|
+
"appdata",
|
|
363
|
+
"localappdata",
|
|
364
|
+
"programfiles",
|
|
365
|
+
"systemroot",
|
|
366
|
+
"windir",
|
|
367
|
+
"shell",
|
|
368
|
+
"term",
|
|
369
|
+
"color",
|
|
370
|
+
"lang",
|
|
371
|
+
"lc_",
|
|
372
|
+
"editor",
|
|
373
|
+
"visual",
|
|
374
|
+
"pager",
|
|
375
|
+
"browser",
|
|
376
|
+
"http",
|
|
377
|
+
"https",
|
|
378
|
+
"proxy",
|
|
379
|
+
"npm_",
|
|
380
|
+
"pnpm_",
|
|
381
|
+
"yarn_",
|
|
382
|
+
"node_",
|
|
383
|
+
"python",
|
|
384
|
+
"virtual_env",
|
|
385
|
+
"conda",
|
|
386
|
+
"cargo",
|
|
387
|
+
"go",
|
|
388
|
+
"java",
|
|
389
|
+
"ssh",
|
|
390
|
+
"gpg",
|
|
391
|
+
"git_",
|
|
392
|
+
"gh_",
|
|
393
|
+
"github_",
|
|
394
|
+
"ci",
|
|
395
|
+
"tf_",
|
|
396
|
+
"aws_",
|
|
397
|
+
"azure",
|
|
398
|
+
"gcloud",
|
|
399
|
+
];
|
|
400
|
+
/**
|
|
401
|
+
* True when the expansion name itself suggests authz / grant store
|
|
402
|
+
* (e.g. $AUTHZ_DIR, $DEFT_AUTHZ_ROOT, %GRANT_STORE%) — residual path without keywords.
|
|
403
|
+
*/
|
|
404
|
+
function hasAuthzPlausibleExpansionName(command) {
|
|
405
|
+
const lower = command.toLowerCase();
|
|
406
|
+
// O(n) scan for $NAME / ${NAME} / %NAME% containing authz/grant/store store-ish tokens.
|
|
407
|
+
for (let i = 0; i < lower.length; i++) {
|
|
408
|
+
const c = lower[i];
|
|
409
|
+
if (c === "$" && i + 1 < lower.length) {
|
|
410
|
+
let j = i + 1;
|
|
411
|
+
if (lower[j] === "{" || lower[j] === "(")
|
|
412
|
+
j++;
|
|
413
|
+
let name = "";
|
|
414
|
+
while (j < lower.length) {
|
|
415
|
+
const ch = lower[j];
|
|
416
|
+
if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_") {
|
|
417
|
+
name += ch;
|
|
418
|
+
j++;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
if (nameLooksAuthzStore(name))
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
if (c === "%" && i + 1 < lower.length) {
|
|
427
|
+
let j = i + 1;
|
|
428
|
+
let name = "";
|
|
429
|
+
while (j < lower.length && lower[j] !== "%") {
|
|
430
|
+
const ch = lower[j];
|
|
431
|
+
if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_") {
|
|
432
|
+
name += ch;
|
|
433
|
+
j++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
if (nameLooksAuthzStore(name))
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
function nameLooksAuthzStore(name) {
|
|
445
|
+
if (name.length === 0)
|
|
446
|
+
return false;
|
|
447
|
+
if (name.includes("authz") ||
|
|
448
|
+
name.includes("grant") ||
|
|
449
|
+
name === "store" ||
|
|
450
|
+
name.endsWith("_store") ||
|
|
451
|
+
name.startsWith("store_") ||
|
|
452
|
+
name.includes("deft_auth") ||
|
|
453
|
+
name.includes("auth_store")) {
|
|
454
|
+
// Exclude ordinary false friends if any appear later.
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Programmatic env write to store: python/node open(os.environ[...]) / process.env patterns
|
|
461
|
+
* that lack shell `$` expansion but still hit authz paths (#3110 residual).
|
|
462
|
+
*/
|
|
463
|
+
function hasProgrammaticAuthzEnvWrite(command, tokens) {
|
|
464
|
+
const lower = command.toLowerCase();
|
|
465
|
+
let hasProg = false;
|
|
466
|
+
for (const t of tokens) {
|
|
467
|
+
const n = normalizeToken(t);
|
|
468
|
+
if (n === "python" ||
|
|
469
|
+
n === "python3" ||
|
|
470
|
+
n === "node" ||
|
|
471
|
+
n === "nodejs" ||
|
|
472
|
+
n === "perl" ||
|
|
473
|
+
n === "ruby" ||
|
|
474
|
+
n === "pwsh" ||
|
|
475
|
+
n === "powershell") {
|
|
476
|
+
hasProg = true;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!hasProg)
|
|
481
|
+
return false;
|
|
482
|
+
// Must look like a write (open/write/writefile/set-content) not a pure read.
|
|
483
|
+
const writeish = lower.includes("open(") ||
|
|
484
|
+
lower.includes(".write") ||
|
|
485
|
+
lower.includes("writefile") ||
|
|
486
|
+
lower.includes("writetext") ||
|
|
487
|
+
lower.includes("set-content") ||
|
|
488
|
+
lower.includes("out-file") ||
|
|
489
|
+
lower.includes("fs.write") ||
|
|
490
|
+
lower.includes("createwritestream") ||
|
|
491
|
+
lower.includes(">>") ||
|
|
492
|
+
lower.includes("mode='w'") ||
|
|
493
|
+
lower.includes('mode="w"') ||
|
|
494
|
+
lower.includes(",'w'") ||
|
|
495
|
+
lower.includes(',"w"');
|
|
496
|
+
if (!writeish)
|
|
497
|
+
return false;
|
|
498
|
+
// Authz-store target only — bare `state.json` alone is ordinary app state (#3110 residual).
|
|
499
|
+
if (lower.includes("authz") ||
|
|
500
|
+
lower.includes("/grants/") ||
|
|
501
|
+
lower.includes("grant-") ||
|
|
502
|
+
lower.includes("deft_auth") ||
|
|
503
|
+
lower.includes("auth_store") ||
|
|
504
|
+
lower.includes(".deft/authz") ||
|
|
505
|
+
lower.includes(".deft\\authz")) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
// os.environ / process.env + authz-store-ish key (not generic "auth"/"store" alone).
|
|
509
|
+
if (lower.includes("os.environ") ||
|
|
510
|
+
lower.includes("process.env") ||
|
|
511
|
+
lower.includes("$env:") ||
|
|
512
|
+
lower.includes("getenv")) {
|
|
513
|
+
if (lower.includes("authz") ||
|
|
514
|
+
lower.includes("grant") ||
|
|
515
|
+
lower.includes("deft_auth") ||
|
|
516
|
+
lower.includes("auth_store")) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Indirect shell FS mutation that can plausibly hit the authz store (#3110).
|
|
524
|
+
* Narrower than "any write + any expansion" (avoids denying `echo > $HOME/out` under UAT)
|
|
525
|
+
* but still catches opaque `$STORE` dest, `rm -rf $STORE`, authz-named expansions,
|
|
526
|
+
* and programmatic os.environ writes. Does **not** flag ordinary cleanup `rm $TMP/x`.
|
|
527
|
+
* O(n) walks — no polynomial regex on input.
|
|
528
|
+
*/
|
|
529
|
+
function hasIndirectAuthzStoreWrite(command, tokens) {
|
|
530
|
+
if (hasProgrammaticAuthzEnvWrite(command, tokens))
|
|
531
|
+
return true;
|
|
532
|
+
if (!hasWriteShape(command, tokens))
|
|
533
|
+
return false;
|
|
534
|
+
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
535
|
+
// Authz-plausible destination text (literal or expanded path segments).
|
|
536
|
+
// Bare `state.json` alone is ordinary app state — require authz/grants/.deft context
|
|
537
|
+
// (Greptile residual: do not deny unrelated expanded state-file writes under UAT).
|
|
538
|
+
if (lower.includes("authz") ||
|
|
539
|
+
lower.includes("/grants/") ||
|
|
540
|
+
lower.includes("grant-") ||
|
|
541
|
+
lower.includes(".deft/authz") ||
|
|
542
|
+
(lower.includes("state.json") &&
|
|
543
|
+
(lower.includes("authz") ||
|
|
544
|
+
lower.includes(".deft") ||
|
|
545
|
+
lower.includes("/grants/") ||
|
|
546
|
+
hasAuthzPlausibleExpansionName(command)))) {
|
|
547
|
+
// Require write shape already true; still need expansion OR already handled by literal path.
|
|
548
|
+
// When expansion is absent, hasAuthzDirShellWrite covers literals; here catch expanded.
|
|
549
|
+
if (hasEnvExpansion(command) || hasAuthzPlausibleExpansionName(command))
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
if (!hasEnvExpansion(command))
|
|
553
|
+
return false;
|
|
554
|
+
// Expansion var **name** suggests store (e.g. $AUTHZ_DIR without "authz" path text after).
|
|
555
|
+
if (hasAuthzPlausibleExpansionName(command))
|
|
556
|
+
return true;
|
|
557
|
+
// Destructive bins: only pure-opaque dest or authz-named expansion — not `rm $TMP/build`.
|
|
558
|
+
let destructive = false;
|
|
559
|
+
for (const t of tokens) {
|
|
560
|
+
const n = normalizeToken(t);
|
|
561
|
+
if (n === "rm" ||
|
|
562
|
+
n === "rmdir" ||
|
|
563
|
+
n === "unlink" ||
|
|
564
|
+
n === "shred" ||
|
|
565
|
+
n === "remove-item" ||
|
|
566
|
+
n === "ri") {
|
|
567
|
+
destructive = true;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (destructive && lastTokenIsOpaqueExpansion(tokens))
|
|
572
|
+
return true;
|
|
573
|
+
// cp/mv/tee/redirect dest is only `$VAR` / `%VAR%` (opaque absolute store path).
|
|
574
|
+
// Skip ordinary well-known env prefixes ($HOME, $TMPDIR, …).
|
|
575
|
+
if (lastTokenIsOpaqueExpansion(tokens)) {
|
|
576
|
+
const last = [...tokens].reverse().find((t) => !t.startsWith("-")) ?? "";
|
|
577
|
+
const bare = last.replace(/['"%${}]/g, "").toLowerCase();
|
|
578
|
+
if (!ORDINARY_EXPANSION_PREFIXES.some((p) => bare === p || bare.startsWith(p))) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
148
584
|
/** Best-effort shell classification for UAT-sensitive ops beyond push/merge. */
|
|
149
585
|
export function classifyShellAuthzOps(command) {
|
|
150
586
|
const cmd = command.trim();
|
|
@@ -180,6 +616,55 @@ export function classifyShellAuthzOps(command) {
|
|
|
180
616
|
found.add("test");
|
|
181
617
|
if (hasDeploy(tokens))
|
|
182
618
|
found.add("deployment");
|
|
619
|
+
// #3110: authz authority CLI + store **writes** (literal / split / $VAR / rm) → settings.
|
|
620
|
+
if (hasAuthzMutatingCli(tokens))
|
|
621
|
+
found.add("settings");
|
|
622
|
+
if (hasAuthzDirShellWrite(cmd, tokens))
|
|
623
|
+
found.add("settings");
|
|
624
|
+
// Split path write: `cd .deft && echo x > authz/state.json` OR `cd .deft/authz && echo x > state.json`
|
|
625
|
+
// OR `cd .deft/authz && cp … grants/x` (write bin without redirect).
|
|
626
|
+
// When the command cds into an authz path, any write shape is settings (relative dest has no "authz" text).
|
|
627
|
+
{
|
|
628
|
+
let cdsIntoAuthz = false;
|
|
629
|
+
for (let ti = 0; ti < tokens.length - 1; ti++) {
|
|
630
|
+
const bin = normalizeToken(tokens[ti]);
|
|
631
|
+
if (bin !== "cd" && bin !== "pushd" && bin !== "set-location" && bin !== "sl")
|
|
632
|
+
continue;
|
|
633
|
+
const dest = pathishToken(tokens[ti + 1]);
|
|
634
|
+
if (dest.includes("authz")) {
|
|
635
|
+
cdsIntoAuthz = true;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (cdsIntoAuthz && hasWriteShape(cmd, tokens)) {
|
|
640
|
+
found.add("settings");
|
|
641
|
+
}
|
|
642
|
+
else if (hasSplitAuthzPath(cmd) && cmd.includes(">")) {
|
|
643
|
+
// Scan every `>` region — not only the last — so a later `> /tmp/x` cannot hide an earlier store write.
|
|
644
|
+
const lower = cmd.toLowerCase().replace(/\\/g, "/");
|
|
645
|
+
for (let i = 0; i < lower.length; i++) {
|
|
646
|
+
if (lower[i] !== ">")
|
|
647
|
+
continue;
|
|
648
|
+
let j = i + 1;
|
|
649
|
+
if (j < lower.length && lower[j] === ">")
|
|
650
|
+
j++;
|
|
651
|
+
let end = j;
|
|
652
|
+
while (end < lower.length &&
|
|
653
|
+
lower[end] !== "|" &&
|
|
654
|
+
lower[end] !== ";" &&
|
|
655
|
+
lower[end] !== "&" &&
|
|
656
|
+
lower[end] !== "\n") {
|
|
657
|
+
end++;
|
|
658
|
+
}
|
|
659
|
+
if (lower.slice(j, end).includes("authz")) {
|
|
660
|
+
found.add("settings");
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (hasIndirectAuthzStoreWrite(cmd, tokens))
|
|
667
|
+
found.add("settings");
|
|
183
668
|
return [...found];
|
|
184
669
|
}
|
|
185
670
|
/** Map a PreToolUse tool name + optional shell command to authz ops. */
|
package/dist/doctor/main.js
CHANGED
|
@@ -517,8 +517,11 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
|
|
|
517
517
|
});
|
|
518
518
|
return false;
|
|
519
519
|
}
|
|
520
|
-
const
|
|
521
|
-
|
|
520
|
+
const codexEnabled = result.registrations.some((entry) => entry.host === "codex" && entry.status !== "disabled");
|
|
521
|
+
const message = `${checkName}: registered and structurally valid` +
|
|
522
|
+
(codexEnabled
|
|
523
|
+
? "; Codex trust is manual-review-required — open `/hooks` to review the project commands"
|
|
524
|
+
: "");
|
|
522
525
|
sink.success(message);
|
|
523
526
|
addFinding({
|
|
524
527
|
severity: "skip",
|
|
@@ -526,8 +529,11 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
|
|
|
526
529
|
check: checkName,
|
|
527
530
|
status: "registered",
|
|
528
531
|
registrations: result.registrations,
|
|
529
|
-
trust_status: "not-
|
|
530
|
-
trust_review:
|
|
532
|
+
trust_status: codexEnabled ? "manual-review-required" : "not-applicable",
|
|
533
|
+
trust_review: codexEnabled
|
|
534
|
+
? "Open `/hooks` in Codex and review the exact project hook commands."
|
|
535
|
+
: null,
|
|
536
|
+
interception_status: "not-directly-verified",
|
|
531
537
|
});
|
|
532
538
|
return true;
|
|
533
539
|
}
|
|
@@ -543,7 +549,25 @@ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams
|
|
|
543
549
|
const liveCheckName = "agent-hooks-live-probe";
|
|
544
550
|
try {
|
|
545
551
|
const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
|
|
546
|
-
|
|
552
|
+
if (result.code !== 0) {
|
|
553
|
+
const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
|
|
554
|
+
sink.warn(message);
|
|
555
|
+
addFinding({
|
|
556
|
+
severity: "warning",
|
|
557
|
+
message,
|
|
558
|
+
check: checkName,
|
|
559
|
+
status: result.code === 2 ? "unavailable" : "incomplete",
|
|
560
|
+
registrations: result.registrations,
|
|
561
|
+
suggestion: "deft update",
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const enabledHosts = result.registrations
|
|
566
|
+
.filter((entry) => entry.status !== "disabled")
|
|
567
|
+
.map((entry) => entry.host);
|
|
568
|
+
const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot, {
|
|
569
|
+
hosts: enabledHosts,
|
|
570
|
+
});
|
|
547
571
|
if (liveResult.code !== 0) {
|
|
548
572
|
const message = `${liveCheckName}: ${liveResult.message.replace(/\s+/g, " ").trim()}`;
|
|
549
573
|
sink.warn(message);
|
|
@@ -557,8 +581,12 @@ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams
|
|
|
557
581
|
});
|
|
558
582
|
return;
|
|
559
583
|
}
|
|
560
|
-
const
|
|
561
|
-
|
|
584
|
+
const codexEnabled = result.registrations.some((entry) => entry.host === "codex" && entry.status !== "disabled");
|
|
585
|
+
const message = `${checkName}: registered, structurally valid, and live probe passed` +
|
|
586
|
+
(codexEnabled
|
|
587
|
+
? "; Codex trust is manual-review-required — open `/hooks` to review the project commands"
|
|
588
|
+
: "") +
|
|
589
|
+
"; direct shim invocation does not verify host interception";
|
|
562
590
|
sink.success(message);
|
|
563
591
|
addFinding({
|
|
564
592
|
severity: "skip",
|
|
@@ -566,9 +594,13 @@ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams
|
|
|
566
594
|
check: liveCheckName,
|
|
567
595
|
status: "registered-and-functional",
|
|
568
596
|
registrations: result.registrations,
|
|
569
|
-
trust_status: "not-
|
|
570
|
-
trust_review:
|
|
597
|
+
trust_status: codexEnabled ? "manual-review-required" : "not-applicable",
|
|
598
|
+
trust_review: codexEnabled
|
|
599
|
+
? "Open `/hooks` in Codex and review the exact project hook commands."
|
|
600
|
+
: null,
|
|
601
|
+
interception_status: "not-directly-verified",
|
|
571
602
|
live_probe: "passed",
|
|
603
|
+
live_probe_duration_ms: liveResult.durationMs,
|
|
572
604
|
});
|
|
573
605
|
}
|
|
574
606
|
catch (cause) {
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { EngineProbeResult } from "../resolution/classify.js";
|
|
|
4
4
|
import type { ResolutionMode } from "../resolution/index.js";
|
|
5
5
|
import type { ResolveUserMdResult } from "../user-config/resolve-user-md.js";
|
|
6
6
|
import type { AgentHookHealthResult } from "../verify-env/agent-hooks.js";
|
|
7
|
-
import type { AgentHookLiveProbeResult } from "../verify-env/agent-hooks-live-probe.js";
|
|
7
|
+
import type { AgentHookLiveProbeResult, AgentHookLiveProbeSeams } from "../verify-env/agent-hooks-live-probe.js";
|
|
8
8
|
export declare const EXIT_CLEAN = 0;
|
|
9
9
|
export declare const EXIT_DRIFT = 1;
|
|
10
10
|
export declare const EXIT_CONFIG_ERROR = 2;
|
|
@@ -153,7 +153,7 @@ export interface DoctorSeams {
|
|
|
153
153
|
/** Read-only agent-host hook registration probe (#2438). */
|
|
154
154
|
readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
|
|
155
155
|
/** Live hook spawn probe for doctor --full (#2852). */
|
|
156
|
-
readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
|
|
156
|
+
readonly probeAgentHooksLive?: (projectRoot: string, seams?: AgentHookLiveProbeSeams) => AgentHookLiveProbeResult;
|
|
157
157
|
/**
|
|
158
158
|
* xBRIEF project-envelope staleness probe (#2971). Injected so doctor can
|
|
159
159
|
* fail closed on 0.6 project JSON under an xbrief/ layout without re-deriving
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session bind of live deposit generation (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Host-agnostic storage:
|
|
5
|
+
* - Default (no sessionId): `.deft/session-bind.json` — single-operator convenience.
|
|
6
|
+
* - With sessionId: `.deft/session-binds/<safeId>.json` — isolated multi-session binds.
|
|
7
|
+
*
|
|
8
|
+
* Multi-agent hosts MUST pass a stable host session identity when binding and
|
|
9
|
+
* reporting so one session cannot certify another as current.
|
|
10
|
+
*/
|
|
11
|
+
import { type BoundGeneration, type LiveGeneration } from "./types.js";
|
|
12
|
+
/** Relative path for the default (no-sessionId) bind record. */
|
|
13
|
+
export declare const SESSION_BIND_REL: string;
|
|
14
|
+
/** Directory for per-session bind records. */
|
|
15
|
+
export declare const SESSION_BINDS_DIR_REL: string;
|
|
16
|
+
export declare function sessionBindPath(projectRoot: string, sessionId?: string | null): string;
|
|
17
|
+
/**
|
|
18
|
+
* Stable filesystem-safe file name for a host session id.
|
|
19
|
+
* Keeps a short prefix for debug, hashes the rest to avoid path injection.
|
|
20
|
+
* Character filter is O(n) (no regex) — CodeQL poly-redos on uncontrolled ids.
|
|
21
|
+
*/
|
|
22
|
+
export declare function safeSessionFileName(sessionId: string): string;
|
|
23
|
+
export interface BindSessionOptions {
|
|
24
|
+
readonly sessionId?: string | null;
|
|
25
|
+
readonly nowIso?: string;
|
|
26
|
+
/**
|
|
27
|
+
* When live generation is missing (legacy deposit), stamp generation 1 from
|
|
28
|
+
* contentVersion before binding. Default true.
|
|
29
|
+
*/
|
|
30
|
+
readonly ensureLive?: boolean;
|
|
31
|
+
/** Used only when ensuring a missing live token. */
|
|
32
|
+
readonly contentVersion?: string;
|
|
33
|
+
readonly stampedBy?: string;
|
|
34
|
+
/**
|
|
35
|
+
* When binding with a sessionId, also write the default bind path.
|
|
36
|
+
* Default **false** — multi-agent isolation requires hosts to report with
|
|
37
|
+
* `--session-id`. Enabling this reopens cross-session false-current (Greptile).
|
|
38
|
+
*/
|
|
39
|
+
readonly alsoWriteDefault?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Host attests that payload surfaces for the live generation were reloaded
|
|
42
|
+
* into the session. Required for trusted readiness. Default false.
|
|
43
|
+
*/
|
|
44
|
+
readonly payloadLoaded?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface ReadBoundOptions {
|
|
47
|
+
/** When set, read only that session's bind (never the default). */
|
|
48
|
+
readonly sessionId?: string | null;
|
|
49
|
+
}
|
|
50
|
+
/** Parse a bound generation record (null if invalid). */
|
|
51
|
+
export declare function parseBoundGeneration(raw: unknown): BoundGeneration | null;
|
|
52
|
+
/** Read the session bind record (null when absent/unreadable). */
|
|
53
|
+
export declare function readBoundGeneration(projectRoot: string, options?: ReadBoundOptions): BoundGeneration | null;
|
|
54
|
+
/**
|
|
55
|
+
* Bind the current live generation into session context.
|
|
56
|
+
*
|
|
57
|
+
* Does not require restarting a shared host runtime — callers re-load payload
|
|
58
|
+
* surfaces into the session and call this (or `freshness:bind`) to rebind.
|
|
59
|
+
*
|
|
60
|
+
* Multi-agent hosts MUST supply `sessionId` so binds do not overwrite each other.
|
|
61
|
+
*/
|
|
62
|
+
export declare function bindSessionGeneration(projectRoot: string, options?: BindSessionOptions): {
|
|
63
|
+
bound: BoundGeneration;
|
|
64
|
+
live: LiveGeneration;
|
|
65
|
+
path: string;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=bind.d.ts.map
|