@yagni-app/code 1.0.6 → 1.0.7

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 (85) hide show
  1. package/README.md +84 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +13 -0
  12. package/dist/extension/index.js +230 -43
  13. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  14. package/dist/extension/mcp/cliConfig.js +1 -1
  15. package/dist/extension/mcp/config.d.ts +24 -2
  16. package/dist/extension/mcp/config.js +75 -3
  17. package/dist/extension/mcp/manager.d.ts +3 -1
  18. package/dist/extension/mcp/manager.js +2 -2
  19. package/dist/extension/mcp/panel.d.ts +0 -1
  20. package/dist/extension/mcp/panel.js +13 -3
  21. package/dist/extension/mcp/startup.js +8 -6
  22. package/dist/extension/permission/gate.d.ts +97 -2
  23. package/dist/extension/permission/gate.js +375 -26
  24. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  25. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  26. package/dist/extension/permissionRules/engine.d.ts +50 -0
  27. package/dist/extension/permissionRules/engine.js +238 -0
  28. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  29. package/dist/extension/permissionRules/loadConfig.js +90 -0
  30. package/dist/extension/permissionRules/parser.d.ts +38 -0
  31. package/dist/extension/permissionRules/parser.js +136 -0
  32. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  33. package/dist/extension/permissionRules/pathRules.js +120 -0
  34. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  35. package/dist/extension/permissionRules/shellRules.js +221 -0
  36. package/dist/extension/pipeline/invocation.d.ts +3 -6
  37. package/dist/extension/pipeline/invocation.js +3 -6
  38. package/dist/extension/pipeline/runner.d.ts +0 -1
  39. package/dist/extension/pipeline/runner.js +6 -14
  40. package/dist/extension/plugins/inventory.d.ts +88 -0
  41. package/dist/extension/plugins/inventory.js +144 -0
  42. package/dist/extension/plugins/panel.d.ts +45 -0
  43. package/dist/extension/plugins/panel.js +293 -0
  44. package/dist/extension/sandbox/bash.d.ts +99 -0
  45. package/dist/extension/sandbox/bash.js +190 -0
  46. package/dist/extension/sandbox/config.d.ts +114 -0
  47. package/dist/extension/sandbox/config.js +366 -0
  48. package/dist/extension/sandbox/manager.d.ts +98 -0
  49. package/dist/extension/sandbox/manager.js +216 -0
  50. package/dist/extension/sandbox/panel.d.ts +111 -0
  51. package/dist/extension/sandbox/panel.js +342 -0
  52. package/dist/extension/sandbox/session.d.ts +85 -0
  53. package/dist/extension/sandbox/session.js +775 -0
  54. package/dist/extension/telemetry/attrs.d.ts +96 -0
  55. package/dist/extension/telemetry/attrs.js +149 -0
  56. package/dist/extension/telemetry/config.d.ts +99 -0
  57. package/dist/extension/telemetry/config.js +193 -0
  58. package/dist/extension/telemetry/index.d.ts +7 -0
  59. package/dist/extension/telemetry/index.js +7 -0
  60. package/dist/extension/telemetry/probe.d.ts +29 -0
  61. package/dist/extension/telemetry/probe.js +122 -0
  62. package/dist/extension/telemetry/register.d.ts +40 -0
  63. package/dist/extension/telemetry/register.js +192 -0
  64. package/dist/extension/telemetry/sdk.d.ts +63 -0
  65. package/dist/extension/telemetry/sdk.js +207 -0
  66. package/dist/extension/telemetry/tracker.d.ts +131 -0
  67. package/dist/extension/telemetry/tracker.js +551 -0
  68. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  69. package/dist/extension/vendor/ignore.d.ts +86 -0
  70. package/dist/extension/vendor/ignore.js +788 -0
  71. package/dist/goHeadless.d.ts +1 -1
  72. package/dist/goHeadless.js +2 -2
  73. package/dist/launch.d.ts +4 -3
  74. package/dist/launch.js +7 -4
  75. package/dist/mcpCommand.d.ts +10 -1
  76. package/dist/mcpCommand.js +42 -10
  77. package/dist/otel.d.ts +67 -90
  78. package/dist/otel.js +152 -195
  79. package/dist/paths.d.ts +13 -0
  80. package/dist/paths.js +18 -0
  81. package/dist/pluginCommand.d.ts +43 -0
  82. package/dist/pluginCommand.js +499 -0
  83. package/dist/pluginStore.d.ts +170 -0
  84. package/dist/pluginStore.js +554 -0
  85. package/package.json +19 -3
@@ -53,32 +53,34 @@ export async function startMcp(pi, opts) {
53
53
  registerMcpPanel(pi, outcome.manager, () => outcome.configErrors);
54
54
  if (servers.length === 0 && errors.length === 0)
55
55
  return outcome;
56
- // Approval gate for project-scope servers (fail-closed: undecided = skip).
56
+ // Approval gate for project-scope servers and repo-sourced plugin servers
57
+ // (fail-closed: undecided = skip).
57
58
  const { state } = readProjectApproval(repoRoot);
58
- const projectNames = servers.filter((s) => s.scope === "project").map((s) => s.name);
59
+ const needsApproval = (s) => s.scope === "project" || s.gated === true;
60
+ const projectNames = servers.filter(needsApproval).map((s) => s.name);
59
61
  const undecided = undecidedProjectServers(state, projectNames);
60
62
  outcome.skippedApproval = undecided;
61
63
  const gate = (server) => {
62
- if (server.scope !== "project")
64
+ if (!needsApproval(server))
63
65
  return true;
64
66
  return !undecided.includes(server.name);
65
67
  };
66
68
  // Ask once, interactively, about undecided project servers (CC parity).
67
69
  if (undecided.length > 0 && opts.hasUI && opts.notify) {
68
- opts.notify(`.mcp.json in this repo defines MCP server(s) ${undecided.join(", ")} — not yet approved. ` +
70
+ opts.notify(`This repo's config (.mcp.json or a repo plugin) defines MCP server(s) ${undecided.join(", ")} — not yet approved. ` +
69
71
  `Run /mcp to approve them individually, or \`yagni mcp reset-project-choices\` to clear old choices.`);
70
72
  }
71
73
  const connectable = [];
72
74
  for (const server of servers) {
73
75
  if (!gate(server)) {
74
- outcome.manager.register(server.name, server.scope, server.config, "disabled");
76
+ outcome.manager.register(server.name, server.scope, server.config, "disabled", server.gated === true);
75
77
  continue;
76
78
  }
77
79
  connectable.push(server);
78
80
  }
79
81
  // Connect in parallel; each server fails soft.
80
82
  const results = await Promise.all(connectable.map(async (server) => {
81
- const managed = outcome.manager.register(server.name, server.scope, server.config, "connecting");
83
+ const managed = outcome.manager.register(server.name, server.scope, server.config, "connecting", server.gated === true);
82
84
  const connected = await outcome.manager.connect(server.name);
83
85
  if (connected?.status === "connected") {
84
86
  const tools = await registerServerTools(pi, outcome.manager, server.name, env);
@@ -26,7 +26,7 @@
26
26
  * When the mode leaves plan, stale plan-context messages are filtered out of
27
27
  * the context so the model doesn't keep believing it is restricted.
28
28
  */
29
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
29
+ import type { ExtensionAPI, ExtensionContext, ToolCallEvent, ToolCallEventResult } from "@earendil-works/pi-coding-agent";
30
30
  import { type ApprovedPrefixGrant } from "./approvedPrefixes.js";
31
31
  import type { HookRunner } from "../hooks.js";
32
32
  import { type BlessStore } from "../bless.js";
@@ -94,6 +94,33 @@ export interface GateDecision {
94
94
  */
95
95
  classifyJustification?: string;
96
96
  }
97
+ /**
98
+ * What the hard floors say about an ALLOW-rule verdict — the three invariants
99
+ * that outrank any user/project permission rule. Extracted from the inline
100
+ * allow path so the gate reads linearly and the floors are directly testable.
101
+ * PURE — no I/O, no session state:
102
+ *
103
+ * "allow" no floor applies — the rule's allow short-circuits everything
104
+ * "block" the exec-policy forbidden band holds despite the allow rule
105
+ * "confirm" alwaysConfirmTools keeps its fresh-consent contract
106
+ * "hold" plan mode's no-mutation contract outranks the allow rule —
107
+ * fall through to the normal plan-mode gate
108
+ *
109
+ * Order is deliberate: plan-mode first (its outcome routes to the EXISTING
110
+ * plan-mode machinery, not a bespoke block); then the terminal forbidden-band
111
+ * block; then the confirm deferral.
112
+ */
113
+ export type AllowRuleFloorOutcome = {
114
+ kind: "allow";
115
+ } | {
116
+ kind: "block";
117
+ reason: string;
118
+ } | {
119
+ kind: "confirm";
120
+ } | {
121
+ kind: "hold";
122
+ };
123
+ export declare function allowRuleFloorVerdict(toolName: string, params: Record<string, unknown>, mode: PermissionMode, policy: PermissionPolicy): AllowRuleFloorOutcome;
97
124
  /**
98
125
  * Pure permission decision for one tool call under a mode + policy. Auto allows
99
126
  * ordinary tools; plan blocks the write/exec set; review marks writes for confirmation
@@ -105,7 +132,7 @@ export declare function decideGate(toolName: string, params: Record<string, unkn
105
132
  * is emitted per terminal outcome; `consulted` says whether a Guardian LLM
106
133
  * call actually happened (grants/cache hits skip it).
107
134
  */
108
- export type GuardianGateOutcome = "prefix_allow" | "cached_allow" | "allow" | "deny" | "ask_approved" | "ask_approved_remembered" | "ask_denied" | "ask_headless_blocked" | "breaker_ask_approved" | "breaker_blocked" | GuardianError;
135
+ export type GuardianGateOutcome = "prefix_allow" | "cached_allow" | "sandbox_auto_allow" | "allow" | "deny" | "ask_approved" | "ask_approved_remembered" | "ask_denied" | "ask_headless_blocked" | "breaker_ask_approved" | "breaker_blocked" | GuardianError;
109
136
  /**
110
137
  * Rich per-decision event for opt-in storage (YAG-510). Carries the RAW
111
138
  * command — the wiring layer (index.ts) hashes/redacts per the workspace's
@@ -129,6 +156,17 @@ export interface GuardianGateEvent {
129
156
  * error-fallback ask that the user then approved). */
130
157
  guardianError?: GuardianError;
131
158
  }
159
+ /** Claude Code's decision sources for a tool permission outcome. */
160
+ export type ToolDecisionSource = "config" | "hook" | "user_permanent" | "user_temporary" | "user_abort" | "user_reject";
161
+ /** One terminal tool_call outcome, in Claude Code's telemetry shape. */
162
+ export interface ToolDecisionEvent {
163
+ toolName: string;
164
+ toolCallId?: string;
165
+ decision: "accept" | "reject";
166
+ source: ToolDecisionSource;
167
+ /** For edit/write tools: the target path (language attribution). */
168
+ filePath?: string;
169
+ }
132
170
  /** What was blessed with "don't ask again", handed to the capture hook. */
133
171
  export interface BlessRememberInfo {
134
172
  tool: string;
@@ -187,12 +225,55 @@ export interface RegisterPermissionDeps {
187
225
  /** Persist a new grant (fire-and-forget; the in-memory list is updated
188
226
  * either way). index.ts wires approvedPrefixes.appendGrant. */
189
227
  persistGrant?: (grant: ApprovedPrefixGrant) => void;
228
+ /**
229
+ * persist a user-level permission rule string (e.g.
230
+ * `Bash(git push:*)`) into ~/.yagni-code/config.json permissions.allow.
231
+ * Wired by index.ts; offered as a third option on Guardian ask dialogs.
232
+ * Fail-soft: the in-session approval applies even if the write fails.
233
+ */
234
+ persistUserRule?: (ruleString: string) => void;
190
235
  /**
191
236
  * Called (fire-and-forget) at every terminal prompt-band outcome with the
192
237
  * rich storage event (raw command — the wiring layer redacts/hashes).
193
238
  * Fail-soft; never blocks.
194
239
  */
195
240
  onGuardianEvent?: (event: GuardianGateEvent) => void;
241
+ /**
242
+ * Called (fire-and-forget) at EVERY terminal tool_call outcome, for every
243
+ * tool, with the Claude Code-shaped decision (accept/reject + source).
244
+ * Feeds the telemetry `tool_decision` event and the
245
+ * `code_edit_tool.decision` counter. Fail-soft; never blocks.
246
+ */
247
+ onToolDecision?: (event: ToolDecisionEvent) => void;
248
+ /**
249
+ * settings-based permission rules (user + project config.json).
250
+ * When present, evaluated at the TOP of the tool_call handler, before
251
+ * hooks: deny → ask → allow, first match wins. Deny/ask are final; allow
252
+ * short-circuits the Guardian but can never lift the exec-policy forbidden
253
+ * band or the alwaysConfirmTools contract (both re-checked below).
254
+ */
255
+ permissionRules?: readonly import("../permissionRules/loadConfig.js").PermissionRule[];
256
+ /** ~/.yagni-code — the user `/`-anchor base for path rules. */
257
+ rulesUserStateHome?: string;
258
+ /** Project root for project-source `/`-anchored path rules; null outside a repo. */
259
+ rulesProjectRoot?: string | null;
260
+ /** Overrides ~ expansion for path rules (tests). */
261
+ rulesHomeDir?: string;
262
+ /** Called (fire-and-soft) after every rule verdict for debug logging. */
263
+ onRuleVerdict?: (event: import("../permissionRules/engine.js").RuleEvaluation & {
264
+ toolName: string;
265
+ cwd: string;
266
+ }) => void;
267
+ /**
268
+ * Sandbox auto-allow (YAGNI Code sandbox): when present and true for this bash call,
269
+ * a prompt-band command runs sandboxed WITHOUT a Guardian consult or user
270
+ * prompt (Claude's autoAllowBashIfSandboxed). Consulted AFTER deny/ask
271
+ * rules, the exec-policy forbidden band, grants, and the session cache —
272
+ * never in plan mode (plan's no-mutation contract outranks it). The
273
+ * caller (session.ts) owns the decision: sandbox initialized + this
274
+ * command will be wrapped + autoAllow enabled.
275
+ */
276
+ sandboxAutoAllow?: (toolName: string, params: Record<string, unknown>) => boolean;
196
277
  /**
197
278
  * User-configurable lifecycle hooks (YAG-506). When present, PreToolUse
198
279
  * hooks run before decideGate and can short-circuit (allow/deny/ask),
@@ -217,9 +298,23 @@ export declare function buildModeContextMessage(mode: PermissionMode): string;
217
298
  export declare function filterStaleModeContext<T>(messages: T[], currentMode?: PermissionMode): T[];
218
299
  /** Legacy alias — the original plan-mode filter name. */
219
300
  export declare const filterStalePlanContext: typeof filterStaleModeContext;
301
+ /** The decision source of ONE tool_call (telemetry), carried per invocation
302
+ * because tool calls run concurrently: refined by the user dialog, hooks,
303
+ * and Guardian outcomes as the call moves through the gate. */
304
+ interface DecisionSlot {
305
+ source: ToolDecisionSource;
306
+ }
307
+ /**
308
+ * The pi tool_call handler wiring (crash-honest attribution). Extracted so
309
+ * the telemetry contract is testable against the production path: a thrown
310
+ * gate records reject — never accept — logs a gate_crashed line (metadata
311
+ * only), and rethrows so pi's handling of a crashed handler is unchanged.
312
+ */
313
+ export declare function wireToolCallGate(pi: Pick<ExtensionAPI, "on">, run: (event: ToolCallEvent, ctx: ExtensionContext, slot: DecisionSlot) => Promise<ToolCallEventResult | undefined>, onToolDecision?: (ev: ToolDecisionEvent) => void): void;
220
314
  /**
221
315
  * Wire the tool_call gate + the /mode command onto a shared mode holder. Default
222
316
  * auto, so absent any /mode this is a no-op over today's behavior.
223
317
  */
224
318
  export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): void;
319
+ export {};
225
320
  //# sourceMappingURL=gate.d.ts.map