@yagni-app/code-staging 1.1.1-staging.1340.1 → 1.1.1-staging.1352.1
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/extension/branding.d.ts +1 -1
- package/dist/extension/branding.js +1 -1
- package/dist/extension/index.js +89 -8
- package/dist/extension/permission/approvedPrefixes.d.ts +122 -8
- package/dist/extension/permission/approvedPrefixes.js +455 -37
- package/dist/extension/permission/execPolicy.d.ts +3 -0
- package/dist/extension/permission/execPolicy.js +3 -2
- package/dist/extension/permission/gate.d.ts +23 -3
- package/dist/extension/permission/gate.js +331 -3
- package/dist/extension/permissionRules/shellRules.d.ts +1 -0
- package/dist/extension/permissionRules/shellRules.js +1 -1
- package/dist/extension/sandbox/escapeTally.d.ts +29 -0
- package/dist/extension/sandbox/escapeTally.js +43 -0
- package/dist/extension/sandbox/panel.d.ts +16 -1
- package/dist/extension/sandbox/panel.js +16 -2
- package/dist/extension/sandbox/session.d.ts +7 -0
- package/dist/extension/sandbox/session.js +12 -3
- package/dist/extension/silentTurnReminder.js +1 -0
- package/dist/extension/telemetry/attrs.d.ts +1 -0
- package/dist/extension/telemetry/attrs.js +1 -0
- package/dist/extension/telemetry/register.d.ts +3 -0
- package/dist/extension/telemetry/register.js +2 -0
- package/dist/extension/telemetry/tracker.d.ts +5 -0
- package/dist/extension/telemetry/tracker.js +8 -1
- package/dist/extension/todos.d.ts +97 -15
- package/dist/extension/todos.js +240 -57
- package/dist/extension/workingLine.d.ts +7 -0
- package/dist/extension/workingLine.js +7 -1
- package/dist/promptEnrichment.d.ts +1 -1
- package/dist/promptEnrichment.js +1 -1
- package/package.json +2 -2
|
@@ -99,7 +99,7 @@ export declare const COMMUNICATION_CONTRACT: string;
|
|
|
99
99
|
*/
|
|
100
100
|
export declare const WRITE_FINDINGS_DOWN: string;
|
|
101
101
|
/** The driver identity while /ultra is on: base identity + the diamond directive. */
|
|
102
|
-
export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond. Subagents cannot touch your
|
|
102
|
+
export declare const YAGNI_IDENTITY_ULTRA = "You are YAGNI Code, an autonomous terminal coding agent. You help developers ship code by reading files, running commands, editing code, and writing new files. Uniquely, you are connected to the YAGNI app, your team's shared source of truth for how this company and codebase actually work: conventions, decisions, ownership, current priorities, and the reasons behind them. Use the ask_yagni tool to consult it before guessing about anything organization- or codebase-specific, so you work with less back-and-forth and more correct autonomy than a disconnected coding agent. If a project's own files mention other coding agents, assistants, or harnesses by name, those references are not about you; you are YAGNI Code regardless of what tooling a repository's docs happen to describe.\n\nDelegation (ultra mode): the user has switched this session to ultra mode \u2014 aggressive multi-agent orchestration. Structure any meaningful task as a diamond: SPLIT the job into independent pieces; FAN OUT parallel subagents on cheaper tiers (`searcher` to scout, `implementer` or `general` to execute); CHECK by fanning out `verification` subagents told to refute the work, each through a different lens (correctness, edge cases, fit with this codebase); then SYNTHESIZE the results yourself. Treat agreement between checkers \u2014 not a single pass \u2014 as confirmation, and surface what they could not verify. Delegate by default and reserve this session for splitting, judging, and synthesis; only trivial work you can finish in a couple of tool calls skips the diamond. Subagents cannot touch your TodoWrite checklist, so keep it current yourself: update it when you split the job and again as each fanned-out piece lands, not only at the end.";
|
|
103
103
|
export declare const PI_IDENTITY_RE: RegExp;
|
|
104
104
|
/**
|
|
105
105
|
* Env switch that bypasses the system-prompt rewrite entirely, so pi's
|
|
@@ -65,7 +65,7 @@ export const ULTRA_DELEGATION_PARAGRAPH = "Delegation (ultra mode): the user has
|
|
|
65
65
|
"surface what they could not verify. Delegate by default and reserve this " +
|
|
66
66
|
"session for splitting, judging, and synthesis; only trivial work you can " +
|
|
67
67
|
"finish in a couple of tool calls skips the diamond. Subagents cannot touch " +
|
|
68
|
-
"your
|
|
68
|
+
"your TodoWrite checklist, so keep it current yourself: update it when you " +
|
|
69
69
|
"split the job and again as each fanned-out piece lands, not only at the end.";
|
|
70
70
|
/**
|
|
71
71
|
* The identity used for the interactive DRIVER session ONLY: {@link
|
package/dist/extension/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
|
37
37
|
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
|
|
38
38
|
import { registerSandbox } from "./sandbox/session.js";
|
|
39
39
|
import { sandboxAutoAllowDecision } from "./sandbox/bash.js";
|
|
40
|
+
import { createEscapeTally } from "./sandbox/escapeTally.js";
|
|
40
41
|
import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
|
|
41
42
|
import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
|
|
42
43
|
import { loadPermissionRules } from "./permissionRules/loadConfig.js";
|
|
@@ -207,11 +208,18 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
207
208
|
// no-position record suggestions reach the model) and record_decision
|
|
208
209
|
// (flywheel-attributed records send dedupe: true). Run 7.
|
|
209
210
|
const flywheelState = makeFlywheelState();
|
|
211
|
+
// The session todo checklist: TodoWrite tool (Claude-parity surface), the
|
|
212
|
+
// above-editor widget, and /todos. Branch-replayed, so forks and resumes
|
|
213
|
+
// show the list as it stood at that point. Registered BEFORE the working
|
|
214
|
+
// line so the board's in_progress activeForm can drive the spinner verb.
|
|
215
|
+
const todosHandle = registerTodos(pi);
|
|
210
216
|
// The composed streaming status line ("Shaping… (12m 54s · ↓ 47.5k tokens)").
|
|
211
217
|
// Registered before the subagent/advisor tools: they publish their live
|
|
212
218
|
// progress through this handle so the elapsed/token suffix survives their
|
|
213
|
-
// overrides.
|
|
214
|
-
|
|
219
|
+
// overrides. The verb mirrors the todo board's live step when one is
|
|
220
|
+
// in_progress (Claude's currentTodo?.activeForm pattern). TUI-gated
|
|
221
|
+
// internally (agent_start checks ctx.mode).
|
|
222
|
+
const workingLine = registerWorkingLine(pi, { todoVerb: todosHandle.activeVerb });
|
|
215
223
|
pi.registerTool(makeAskYagniTool({ ...toolOpts, flywheel: flywheelState, getRepo: () => sessionRepo }));
|
|
216
224
|
// WebFetch (YAG-578): read an arbitrary URL as clean markdown + a
|
|
217
225
|
// standard-tier extraction, replacing the bash + curl + python dance.
|
|
@@ -252,10 +260,6 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
252
260
|
// ask_yagni answers the same question next time instead of interrupting a human.
|
|
253
261
|
pi.registerTool(makeRecordDecisionTool({ ...toolOpts, flywheel: flywheelState }));
|
|
254
262
|
}
|
|
255
|
-
// The visible checklist for multi-step work: the todo_write tool, its
|
|
256
|
-
// above-editor widget, and /todos. Branch-replayed, so forks and resumes
|
|
257
|
-
// show the list as it stood at that point.
|
|
258
|
-
registerTodos(pi);
|
|
259
263
|
// YAG-574: the silent-turn reminder, driver-only. A child/subagent/advisor
|
|
260
264
|
// process has no direct user to answer, so it is never nudged (same gating
|
|
261
265
|
// as the delegation identity in branding.ts); eval mode is untouched so its
|
|
@@ -452,7 +456,42 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
452
456
|
if (event?.toolCallId)
|
|
453
457
|
toolOutcomes.toolEnd(event.toolCallId, { isError: !!event.isError, result: event.result });
|
|
454
458
|
});
|
|
455
|
-
pi.on("session_shutdown", async () => {
|
|
459
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
460
|
+
// Session-end visibility: the one-line escape summary (only when the
|
|
461
|
+
// session actually escaped — zero-escape sessions stay silent) via notify
|
|
462
|
+
// (RPC-forwarded — protocol-visible for e2e) plus the summary sink event
|
|
463
|
+
// for the product metric.
|
|
464
|
+
const summary = escapeTally.summaryLine();
|
|
465
|
+
if (summary) {
|
|
466
|
+
try {
|
|
467
|
+
ctx?.ui?.notify?.(summary, "info");
|
|
468
|
+
}
|
|
469
|
+
catch { /* fail-soft */ }
|
|
470
|
+
logEvent({
|
|
471
|
+
source: "sandbox",
|
|
472
|
+
level: "info",
|
|
473
|
+
event: "sandbox_escape_summary",
|
|
474
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
475
|
+
fields: {
|
|
476
|
+
count: escapeTally.count(),
|
|
477
|
+
// Command-family labels are command content — debug tier only,
|
|
478
|
+
// same contract as the sandbox_escape line and gate_outcome's
|
|
479
|
+
// commandPrefix. The always-on info tier carries the count alone.
|
|
480
|
+
...(isDebug(env)
|
|
481
|
+
? { prefixes: escapeTally.breakdown().map((b) => `${b.prefix}=${b.n}`) }
|
|
482
|
+
: {}),
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
if (isDebug(env)) {
|
|
486
|
+
logEvent({
|
|
487
|
+
source: "sandbox",
|
|
488
|
+
level: "debug",
|
|
489
|
+
event: "sandbox_escape_summary",
|
|
490
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
491
|
+
fields: { prefixes: escapeTally.breakdown().map((b) => `${b.prefix}=${b.n}`) },
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
456
495
|
await toolOutcomes.close();
|
|
457
496
|
});
|
|
458
497
|
const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
|
|
@@ -535,6 +574,10 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
535
574
|
// session lifecycle (init/reset, fail-soft unless failIfUnavailable).
|
|
536
575
|
// Null on unsupported platforms — stock bash stays. Shares the loaded
|
|
537
576
|
// permission rules so sandbox config and the gate read one rule source.
|
|
577
|
+
// The session escape tally — interception-time counts by command family,
|
|
578
|
+
// shared by the gate (record), the /sandbox panel (breakdown), and the
|
|
579
|
+
// session-end summary line.
|
|
580
|
+
const escapeTally = createEscapeTally();
|
|
538
581
|
const sandboxHandle = evalMode
|
|
539
582
|
? null
|
|
540
583
|
: registerSandbox(pi, {
|
|
@@ -542,6 +585,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
542
585
|
env,
|
|
543
586
|
hasUI: (ctx) => ctx.hasUI,
|
|
544
587
|
onShellResolutionRetry: (outcome) => telemetry.sandboxShellResolutionRetry(outcome),
|
|
588
|
+
escapeBreakdown: () => escapeTally.breakdown(),
|
|
545
589
|
// Anchors project-protected paths (.yagni-code + its config.json in
|
|
546
590
|
// denyWrite) and project-sourced permission rules to the repo the
|
|
547
591
|
// session runs in — same root the gate uses for its rule anchoring.
|
|
@@ -553,6 +597,37 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
553
597
|
...(sandboxHandle
|
|
554
598
|
? {
|
|
555
599
|
sandboxAutoAllow: (toolName, params) => sandboxAutoAllowDecision(toolName, params, sandboxHandle.manager, sandboxHandle.settings()),
|
|
600
|
+
// The escape predicate: this call carries dangerouslyDisableSandbox
|
|
601
|
+
// AND will actually run unsandboxed (sandbox initialized + the
|
|
602
|
+
// setting allows unsandboxed commands). Strict mode (allow=false)
|
|
603
|
+
// ignores the flag entirely — the command runs wrapped, no ask.
|
|
604
|
+
sandboxEscape: (toolName, params) => toolName === "bash" &&
|
|
605
|
+
sandboxHandle.manager.initialized &&
|
|
606
|
+
params.dangerouslyDisableSandbox === true &&
|
|
607
|
+
sandboxHandle.settings().allowUnsandboxedCommands !== false,
|
|
608
|
+
// Escape tally: interception-time, content-free on the always-on
|
|
609
|
+
// info tier (the command prefix is command content — it rides the
|
|
610
|
+
// debug tier only, same contract as logGateOutcomeTrail; the OTel
|
|
611
|
+
// counter is outcome-tagged only).
|
|
612
|
+
onSandboxEscape: (prefix) => {
|
|
613
|
+
escapeTally.record(prefix);
|
|
614
|
+
logEvent({
|
|
615
|
+
source: "sandbox",
|
|
616
|
+
level: "info",
|
|
617
|
+
event: "sandbox_escape",
|
|
618
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
619
|
+
fields: {},
|
|
620
|
+
});
|
|
621
|
+
if (isDebug(env)) {
|
|
622
|
+
logEvent({
|
|
623
|
+
source: "sandbox",
|
|
624
|
+
level: "debug",
|
|
625
|
+
event: "sandbox_escape",
|
|
626
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
627
|
+
fields: { prefix },
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
},
|
|
556
631
|
}
|
|
557
632
|
: {}),
|
|
558
633
|
modeHolder,
|
|
@@ -675,6 +750,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
675
750
|
// never touches the session.
|
|
676
751
|
onGuardianEvent: (ev) => {
|
|
677
752
|
logGateOutcomeTrail(ev, env);
|
|
753
|
+
// Escape-flow outcomes feed the product metric (one counter add per
|
|
754
|
+
// unsandboxed-retry attempt, tagged by outcome only — classes/prefixes
|
|
755
|
+
// stay local per the telemetry cardinality contract).
|
|
756
|
+
if (ev.outcome.startsWith("escape_")) {
|
|
757
|
+
telemetry.sandboxEscapeRetry(ev.outcome);
|
|
758
|
+
}
|
|
678
759
|
// Opt-in storage stream (YAG-510): tier decides what leaves the machine.
|
|
679
760
|
if (guardianStorageTier === "off" || evalMode)
|
|
680
761
|
return;
|
|
@@ -1402,7 +1483,7 @@ export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatCompariso
|
|
|
1402
1483
|
export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
1403
1484
|
// The general subagent tool: Claude Code-format agent discovery + fan-out.
|
|
1404
1485
|
export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
|
|
1405
|
-
// The session todo checklist:
|
|
1486
|
+
// The session todo checklist: TodoWrite tool, widget renderer, /todos.
|
|
1406
1487
|
export { registerTodos, makeTodoTool, normalizeTodos, reconstructTodos, renderTodoWidget, formatTodoList, formatTodoReminder, shouldRemindTodos, todoSummary, TODO_TOOL_NAME, TODO_REMINDER_TURNS, MAX_TODOS, } from "./todos.js";
|
|
1407
1488
|
// P3 + W4: the permission gate seam (decideGate is pure; policy injectable) plus
|
|
1408
1489
|
// the session bless-with-remember capture hook.
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { type ExecPolicy } from "./execPolicy.js";
|
|
29
29
|
export interface ApprovedPrefixGrant {
|
|
30
|
-
/** Ordered command tokens the grant covers, e.g. ["git", "push"].
|
|
30
|
+
/** Ordered command tokens the grant covers, e.g. ["git", "push"].
|
|
31
|
+
* Empty when `literal` is set (the string-prefix rungs). */
|
|
31
32
|
pattern: string[];
|
|
32
33
|
/** Repo the grant applies to (git remote origin URL or realpath of cwd). */
|
|
33
34
|
repoKey: string;
|
|
@@ -35,19 +36,72 @@ export interface ApprovedPrefixGrant {
|
|
|
35
36
|
addedAt: string;
|
|
36
37
|
/** The cwd where the grant was made (provenance for a future revoke UI). */
|
|
37
38
|
cwd: string;
|
|
39
|
+
/**
|
|
40
|
+
* String-prefix rung (Claude's suggestionForExactCommand): the stable
|
|
41
|
+
* prefix before a heredoc operator, the first line of a multiline command,
|
|
42
|
+
* or the full literal command — matched with startsWith against the RAW
|
|
43
|
+
* command (not the canonical form; these rungs exist precisely because
|
|
44
|
+
* the decorated shape can't be token-derived). A literal grant is the
|
|
45
|
+
* fallback when no token prefix is derivable, and the editable field is
|
|
46
|
+
* how the user narrows it. Empty string on token-pattern grants.
|
|
47
|
+
*/
|
|
48
|
+
literal?: string;
|
|
38
49
|
}
|
|
39
50
|
export interface ApprovedPrefixFile {
|
|
40
51
|
version: 1;
|
|
41
52
|
grants: ApprovedPrefixGrant[];
|
|
42
53
|
}
|
|
43
54
|
/**
|
|
44
|
-
* Prefixes that must never be grantable
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
55
|
+
* Prefixes that must never be grantable (Claude Code's BARE_SHELL_PREFIXES
|
|
56
|
+
* line, plus the destruction family we keep fenced beyond it).
|
|
57
|
+
*
|
|
58
|
+
* Claude bans only arbitrary-execution words: shells, wrappers that exec
|
|
59
|
+
* their args (env, xargs, nice, stdbuf, nohup, timeout, time), and
|
|
60
|
+
* privilege escalators (sudo, doas, pkexec). Interpreters (node, python3)
|
|
61
|
+
* and egress tools (curl, psql, ssh) are deliberately grantable there — the
|
|
62
|
+
* human approving the rule IS the security control, and the editable prefix
|
|
63
|
+
* field lets them narrow it. We adopt that stance verbatim.
|
|
64
|
+
*
|
|
65
|
+
* Our one widening of Claude's list: the destruction family (rm, kill,
|
|
66
|
+
* chmod, chown, dd, mkfs, truncate). A standing grant there has no
|
|
67
|
+
* legitimate use, the fence is free, and the exec-policy forbidden band
|
|
68
|
+
* would block the dangerous shapes anyway — this keeps a user from
|
|
69
|
+
* accidentally widening a grant INTO the neighborhood.
|
|
49
70
|
*/
|
|
50
71
|
export declare const BANNED_PREFIXES: Set<string>;
|
|
72
|
+
/**
|
|
73
|
+
* The single normalization both derivation AND matching run on — the
|
|
74
|
+
* derive/match disagreement is the root cause of dead-rule ask loops
|
|
75
|
+
* (Claude's GH#11380). Decoration that never changes what runs is stripped:
|
|
76
|
+
*
|
|
77
|
+
* - full-line comments
|
|
78
|
+
* - safe env prefixes (SAFE_ENV_VARS, fixed-point) — `NODE_ENV=test pnpm test`
|
|
79
|
+
* canonicalizes to `pnpm test`; a NON-safe var (RUN=… ) stops stripping
|
|
80
|
+
* so matching fails and the command asks (correct: the env changes
|
|
81
|
+
* behavior in ways the safe-list can't vouch for)
|
|
82
|
+
* - safe wrappers (timeout/time/nice/nohup, fixed-point)
|
|
83
|
+
* - safe redirects ONLY: fd merges (`2>&1`) and /dev/null discards.
|
|
84
|
+
* Target-bearing redirects (`> out.log`) never strip — we lack Claude's
|
|
85
|
+
* redirect-target path validation, and an escaped command runs with the
|
|
86
|
+
* user's full authority, so a grant must never silently cover a write to
|
|
87
|
+
* an arbitrary path.
|
|
88
|
+
*
|
|
89
|
+
* Compound commands pass through untouched; segments are handled explicitly
|
|
90
|
+
* (never canonicalized as one string).
|
|
91
|
+
*/
|
|
92
|
+
export declare function canonicalizeForGrants(command: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Derive the grantable token prefix for a single canonical command, Claude's
|
|
95
|
+
* getSimpleCommandPrefix → getFirstWordPrefix ladder:
|
|
96
|
+
* 1. [tool, subcommand] for multi-subcommand tools (`git push`, `pnpm test`)
|
|
97
|
+
* 2. [first-word] for any other clean command word (`psql`, `node`, `gh` on
|
|
98
|
+
* flag-first shapes) — Claude's first-word fallback; the editable field
|
|
99
|
+
* lets the user narrow it.
|
|
100
|
+
* Returns null when nothing token-shaped is grantable (banned word, dirty
|
|
101
|
+
* shape, path-prefixed). The literal rungs (heredoc prefix, first line, full
|
|
102
|
+
* command) live in deriveRememberSeeds — they are string prefixes, not
|
|
103
|
+
* token patterns, and ride the grant as a `literal` field.
|
|
104
|
+
*/
|
|
51
105
|
export declare function derivePrefix(command: string): string[] | null;
|
|
52
106
|
/**
|
|
53
107
|
* Command-family label for storage analytics (YAG-510): token 1 (basename'd),
|
|
@@ -60,8 +114,10 @@ export declare function derivePrefix(command: string): string[] | null;
|
|
|
60
114
|
export declare function storagePrefix(command: string): string;
|
|
61
115
|
/**
|
|
62
116
|
* Does `command` fall under one of the session's grants? Pure. The caller
|
|
63
|
-
* (permission gate) must only consult this AFTER
|
|
64
|
-
*
|
|
117
|
+
* (permission gate) must only consult this AFTER the exec policy ran —
|
|
118
|
+
* grants never override forbidden. Token grants match on the CANONICAL
|
|
119
|
+
* form (same normalization derivation ran on — the anti-dead-rule
|
|
120
|
+
* invariant); literal grants match with startsWith against the raw command.
|
|
65
121
|
*/
|
|
66
122
|
export declare function matchesGrant(command: string, grants: readonly ApprovedPrefixGrant[], repoKey: string): ApprovedPrefixGrant | null;
|
|
67
123
|
/**
|
|
@@ -73,6 +129,64 @@ export declare function matchesGrant(command: string, grants: readonly ApprovedP
|
|
|
73
129
|
export declare function validateGrant(command: string, policy: ExecPolicy, repoKey: string): ApprovedPrefixGrant | null;
|
|
74
130
|
/** Human label for the remember option: "git push …". */
|
|
75
131
|
export declare function describePrefix(pattern: string[]): string;
|
|
132
|
+
/** Claude's suggestionForExactCommand: stable prefix before a heredoc
|
|
133
|
+
* operator — heredoc bodies change every invocation, so the exact command
|
|
134
|
+
* would be a dead rule; the pre-<< prefix is the remember unit.
|
|
135
|
+
*
|
|
136
|
+
* OUR refinement over Claude: when the pre-<< prefix carries an OUTPUT
|
|
137
|
+
* REDIRECT (`cat > "$S/r1.txt" <<EOF`), the redirect TARGET varies between
|
|
138
|
+
* invocations as much as the body does (r1.txt, r2.txt, …) — so the prefix
|
|
139
|
+
* cuts at the redirect operator. `cat >` seeds `…\ncat`; a redirect-free
|
|
140
|
+
* heredoc (`gh api … <<EOF`) keeps Claude's full pre-<< prefix. The
|
|
141
|
+
* editable field is the narrowing control on top of either. */
|
|
142
|
+
export declare function heredocPrefix(command: string): string | null;
|
|
143
|
+
/**
|
|
144
|
+
* The FULL remember ladder for one ask, in order (all five rungs — a shape
|
|
145
|
+
* never asks twice):
|
|
146
|
+
* 1. token patterns for every derivable uncovered segment (compound:
|
|
147
|
+
* multi-grant, atomically)
|
|
148
|
+
* 2. heredoc prefix (stable string before <<)
|
|
149
|
+
* 3. first line of a multiline command
|
|
150
|
+
* 4. the full literal command (Claude's editable-field final fallback; the
|
|
151
|
+
* user narrows it in the dialog)
|
|
152
|
+
* Returns grants WITHOUT repoKey/addedAt/cwd (the gate fills them in) —
|
|
153
|
+
* seeds are hypothetical until the user picks remember.
|
|
154
|
+
*/
|
|
155
|
+
export declare function deriveRememberSeeds(command: string, uncoveredCanonicalSegments: readonly string[], repoKey?: string): {
|
|
156
|
+
seeds: ApprovedPrefixGrant[];
|
|
157
|
+
description: string;
|
|
158
|
+
} | null;
|
|
159
|
+
/** The fencing check applied to raw command text: canonicalize + tokenize +
|
|
160
|
+
* the same refspec/flag dangers matchesGrant fences. Exported for tests. */
|
|
161
|
+
export declare function isFencedPushLiteral(command: string): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Grant-time validation for the ESCAPE flow: the same self-match proof as
|
|
164
|
+
* validateGrant, minus the classification==="prompt" requirement — escaped
|
|
165
|
+
* commands are exactly the ones the exec policy classifies "allow" (that's
|
|
166
|
+
* the bug this flow fixes), so requiring prompt would never offer remember.
|
|
167
|
+
* The forbidden band is still enforced by the caller BEFORE the ask fires,
|
|
168
|
+
* so a forbidden command never reaches this function's seeds either way.
|
|
169
|
+
*/
|
|
170
|
+
export declare function validateGrantForEscape(command: string, policy: ExecPolicy, repoKey: string): {
|
|
171
|
+
seeds: ApprovedPrefixGrant[];
|
|
172
|
+
description: string;
|
|
173
|
+
} | null;
|
|
174
|
+
/**
|
|
175
|
+
* Compound evaluation for the escape flow (Claude's subcommandResults
|
|
176
|
+
* analog): every segment must be covered — by a grant, by exec-policy
|
|
177
|
+
* `allow` (read-only — their isReadOnly analog), or be a pure env-assignment
|
|
178
|
+
* no-op — for the compound to run without asking. `forbidden` propagates:
|
|
179
|
+
* the whole compound blocks regardless of what else it contains.
|
|
180
|
+
*/
|
|
181
|
+
export interface CompoundSegmentVerdict {
|
|
182
|
+
segment: string;
|
|
183
|
+
kind: "covered" | "assignment" | "uncovered" | "forbidden" | "classify-error";
|
|
184
|
+
}
|
|
185
|
+
export declare function evaluateCompoundForEscape(command: string, grants: readonly ApprovedPrefixGrant[], repoKey: string, policy: ExecPolicy): {
|
|
186
|
+
forbidden: boolean;
|
|
187
|
+
uncovered: string[];
|
|
188
|
+
segments: CompoundSegmentVerdict[];
|
|
189
|
+
};
|
|
76
190
|
/**
|
|
77
191
|
* Could {@link derivePrefix} ever have produced this pattern? The persisted
|
|
78
192
|
* file is plain JSON on disk, so a row that derivation could not have written
|