@yagni-app/code-staging 1.0.6-staging.1248.1 → 1.0.6-staging.1255.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/README.md +71 -46
- package/dist/cli.js +4 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +52 -27
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +10 -0
- package/dist/extension/index.js +127 -43
- package/dist/extension/permission/gate.d.ts +44 -2
- package/dist/extension/permission/gate.js +107 -25
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +775 -0
- package/dist/extension/telemetry/attrs.d.ts +72 -0
- package/dist/extension/telemetry/attrs.js +125 -0
- package/dist/extension/telemetry/config.d.ts +99 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +40 -0
- package/dist/extension/telemetry/register.js +192 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +530 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +3 -4
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +10 -0
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -173,62 +173,87 @@ environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
|
|
|
173
173
|
|
|
174
174
|
### OTel export (opt-in)
|
|
175
175
|
|
|
176
|
-
Point sessions at **your own** OpenTelemetry collector (Datadog
|
|
177
|
-
Alloy,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
`
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
176
|
+
Point sessions at **your own** OpenTelemetry collector (Datadog, Grafana
|
|
177
|
+
Cloud, a Datadog Agent or Grafana Alloy, any OTLP-native backend) and every
|
|
178
|
+
session — including `/go` stage children and subagents — exports three
|
|
179
|
+
signals in the shape Claude Code's telemetry uses, under a `yagni_code`
|
|
180
|
+
prefix instead of `claude_code`:
|
|
181
|
+
|
|
182
|
+
- **Traces:** a per-prompt span tree, `yagni_code.interaction` →
|
|
183
|
+
`yagni_code.llm_request` and `yagni_code.tool`, carrying both the flat
|
|
184
|
+
Claude Code attributes (`model`, `input_tokens`, `cost_usd`, `tool_name`)
|
|
185
|
+
and the OTel GenAI conventions (`gen_ai.*`) that LLM-observability
|
|
186
|
+
products read.
|
|
187
|
+
- **Metrics:** `yagni_code.session.count`, `token.usage` (by `type` and
|
|
188
|
+
`model`), `cost.usage`, `lines_of_code.count`, `commit.count`,
|
|
189
|
+
`pull_request.count`, `code_edit_tool.decision`, `active_time.total`.
|
|
190
|
+
- **Log events:** `user_prompt`, `assistant_response`, `api_request`,
|
|
191
|
+
`api_error`, `tool_result`, `tool_decision`, `permission_mode_changed`
|
|
192
|
+
(as `event.name`, body `yagni_code.<name>`), with `session.id`,
|
|
193
|
+
`organization.id`, `terminal.type`, and a per-prompt `prompt.id`.
|
|
194
|
+
|
|
195
|
+
Nothing is exported unless you configure an endpoint. Enable it one of three
|
|
196
|
+
ways (first match wins):
|
|
197
|
+
|
|
198
|
+
- the standard OTel env in your shell — the generic
|
|
199
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` (a base URL; `/v1/<signal>` is appended for
|
|
200
|
+
http transports) or the per-signal
|
|
201
|
+
`OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT` a Claude Code setup
|
|
202
|
+
already carries — so a machine configured for Claude Code lights up with
|
|
203
|
+
no extra config (with per-signal endpoints only, exactly those signals
|
|
204
|
+
export; nothing falls back to a localhost collector), or
|
|
192
205
|
- **workspace settings** (the zero-setup path): a workspace admin sets the
|
|
193
206
|
endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
|
|
194
|
-
in the web app under Settings → YAGNI Code →
|
|
195
|
-
the workspace picks it up at launch — nothing to install or
|
|
196
|
-
developer machines. Header values are encrypted at rest
|
|
197
|
-
launch-time copy is cached at mode `0600`, the same
|
|
198
|
-
token. Or,
|
|
199
|
-
- commit `{ "otel": { "endpoint": "http://<collector>:4317" } }`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
in the web app under Settings → YAGNI Code → Telemetry export. Every
|
|
208
|
+
session in the workspace picks it up at launch — nothing to install or
|
|
209
|
+
configure on developer machines. Header values are encrypted at rest
|
|
210
|
+
server-side and the launch-time copy is cached at mode `0600`, the same
|
|
211
|
+
posture as your device token. Or,
|
|
212
|
+
- commit `{ "otel": { "endpoint": "http://<collector>:4317" } }` (optionally
|
|
213
|
+
with `protocol`, `headers`, `serviceName`) to the repo's `.pi/settings.json`
|
|
214
|
+
so one repo's sessions export without per-machine setup. Header values
|
|
215
|
+
must not contain commas (they travel as a comma-separated list); one that
|
|
216
|
+
does is dropped.
|
|
217
|
+
|
|
218
|
+
The rest of the standard OTel env is honored the way Claude Code honors it:
|
|
219
|
+
`OTEL_EXPORTER_OTLP_PROTOCOL` (and per-signal), `OTEL_EXPORTER_OTLP_HEADERS`
|
|
220
|
+
(and per-signal), `OTEL_SERVICE_NAME` (defaults to `yagni-code`),
|
|
221
|
+
`OTEL_RESOURCE_ATTRIBUTES`, `OTEL_{METRICS,LOGS,TRACES}_EXPORTER=none` to
|
|
222
|
+
switch one signal off, `OTEL_METRIC_EXPORT_INTERVAL` /
|
|
223
|
+
`OTEL_LOGS_EXPORT_INTERVAL`, `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`
|
|
224
|
+
(default `delta`), and the `OTEL_METRICS_INCLUDE_*` cardinality flags.
|
|
225
|
+
`YAGNI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
|
|
226
|
+
export state and sends one real record per signal, printing the collector's
|
|
227
|
+
own verdict (a 403 is a bad key, a 404 a bad path or site).
|
|
228
|
+
|
|
229
|
+
Three recipes for common collectors:
|
|
230
|
+
|
|
231
|
+
- **Datadog, direct SaaS (no agent):** endpoint `https://otlp.datadoghq.com`
|
|
232
|
+
(US1; other sites have their own host — `otlp.us3.datadoghq.com`,
|
|
233
|
+
`otlp.us5.datadoghq.com`, `otlp.datadoghq.eu`, `otlp.ap1.datadoghq.com`),
|
|
234
|
+
protocol `http/protobuf` (Datadog's direct intake does not accept gRPC),
|
|
235
|
+
header `dd-api-key=<your Datadog API key>`. Add `dd-otlp-source=llmobs`
|
|
236
|
+
(the workspace preset does) to have the traces routed into LLM
|
|
237
|
+
Observability as well as APM; the launcher scopes that header to the
|
|
238
|
+
traces request. Traces show in APM under service `yagni-code`, metrics
|
|
239
|
+
under `yagni_code.*`, events in Log Explorer as `service:yagni-code`.
|
|
217
240
|
- **Datadog Agent:** endpoint `http://<agent-host>:4317`, protocol `grpc`, no
|
|
218
241
|
key (the agent forwards with its own). OTLP ingest ships in the agent but
|
|
219
242
|
is off by default — enable it with
|
|
220
243
|
`otlp_config.receiver.protocols.grpc.endpoint: 0.0.0.0:4317` in
|
|
221
244
|
`datadog.yaml`.
|
|
222
|
-
- **Grafana Cloud:** endpoint `https://otlp-gateway-prod
|
|
223
|
-
|
|
224
|
-
|
|
245
|
+
- **Grafana Cloud:** endpoint `https://otlp-gateway-prod-<region>.grafana.net/otlp`
|
|
246
|
+
(the base URL from your stack's OTLP page — signals go to `/otlp/v1/traces`
|
|
247
|
+
and friends), protocol `http/protobuf`, header `authorization=<the raw
|
|
248
|
+
MTgx… value>` (the `Basic ` scheme is added for you).
|
|
225
249
|
|
|
226
250
|
Two things are enforced and not configurable:
|
|
227
251
|
|
|
228
|
-
- **Metadata only.** Token counts, cost, tier,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
- **Metadata only.** Token counts, cost, tier, durations, tool names, and
|
|
253
|
+
payload sizes export; prompt text, model output, tool arguments, and tool
|
|
254
|
+
results never do — `OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_TOOL_DETAILS` are
|
|
255
|
+
deliberately not honored.
|
|
256
|
+
- **Cost is your contracted rate.** `cost_usd` is computed from your
|
|
232
257
|
workspace's tier rate card, and the exported model name is the opaque tier
|
|
233
258
|
id (`advanced`, `peak`, …), so traces never fingerprint the backing model.
|
|
234
259
|
|
package/dist/cli.js
CHANGED
|
@@ -247,10 +247,10 @@ async function runDefault(passthroughArgs) {
|
|
|
247
247
|
catch {
|
|
248
248
|
compat = { argv: [], env: {} };
|
|
249
249
|
}
|
|
250
|
-
// OTel export:
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
250
|
+
// OTel export: on only when an OTLP endpoint is configured — the user's
|
|
251
|
+
// env, the workspace's admin-set config (fetched fail-soft, cached), or the
|
|
252
|
+
// repo's .pi/settings.json, in that order. undefined keeps the launch
|
|
253
|
+
// untouched. See otel.ts for the policy.
|
|
254
254
|
const otel = await resolveOtelLaunchWithWorkspace({
|
|
255
255
|
env: process.env,
|
|
256
256
|
cwd: process.cwd(),
|
package/dist/doctor.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Advisory checks (loose perms, missing `gh`) never flip the exit code.
|
|
14
14
|
*/
|
|
15
15
|
import { type TokenExpiryStatus } from "./launch.js";
|
|
16
|
-
import {
|
|
16
|
+
import { type OtelLaunchConfig } from "./otel.js";
|
|
17
17
|
import { type Profile } from "./profiles.js";
|
|
18
18
|
export type CheckStatus = "ok" | "warn" | "fail";
|
|
19
19
|
export interface CheckResult {
|
|
@@ -61,16 +61,25 @@ export declare function checkCliUpdate(probe: {
|
|
|
61
61
|
latest: string | null;
|
|
62
62
|
}): CheckResult;
|
|
63
63
|
export declare function checkGh(onPath: boolean): CheckResult;
|
|
64
|
+
/** One signal's verdict from the extension's probe (see telemetry/probe.ts). */
|
|
65
|
+
export interface OtelProbeSignal {
|
|
66
|
+
signal: "traces" | "metrics" | "logs";
|
|
67
|
+
url: string;
|
|
68
|
+
protocol: string;
|
|
69
|
+
ok: boolean;
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
64
72
|
/**
|
|
65
|
-
* Advisory OTel-export line: says whether sessions will stream
|
|
66
|
-
* OTLP collector, from which config source, and — when a collector is
|
|
67
|
-
* configured — whether
|
|
68
|
-
*
|
|
69
|
-
* most machines have no collector, and
|
|
73
|
+
* Advisory OTel-export line: says whether sessions will stream telemetry to
|
|
74
|
+
* an OTLP collector, from which config source, and — when a collector is
|
|
75
|
+
* configured — whether it ACCEPTED a real record on each signal (a wrong API
|
|
76
|
+
* key, a wrong path, or a protocol mismatch shows up as the collector's own
|
|
77
|
+
* error). Never flips the exit code — most machines have no collector, and
|
|
78
|
+
* that is the healthy default.
|
|
70
79
|
*/
|
|
71
80
|
export declare function checkOtelExport(config: OtelLaunchConfig | undefined, deps?: {
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
probe?: (env: NodeJS.ProcessEnv) => Promise<OtelProbeSignal[]>;
|
|
82
|
+
baseEnv?: NodeJS.ProcessEnv;
|
|
74
83
|
}): Promise<CheckResult>;
|
|
75
84
|
/** Config-only MCP snapshot (no connections — health checks live in `mcp list`/`get`). */
|
|
76
85
|
export interface McpProbe {
|
|
@@ -120,6 +129,10 @@ export interface DoctorDeps {
|
|
|
120
129
|
probeLatestVersion?: () => Promise<string | null>;
|
|
121
130
|
/** MCP config snapshot (config-only, no connections). */
|
|
122
131
|
probeMcp?: () => Promise<McpProbe>;
|
|
132
|
+
/** OTel gate resolution (env / workspace / repo settings); tests stub it. */
|
|
133
|
+
resolveOtel?: (profile: Profile) => Promise<OtelLaunchConfig | undefined>;
|
|
134
|
+
/** OTel export probe: one real record per signal against the session env. */
|
|
135
|
+
probeOtel?: (env: NodeJS.ProcessEnv) => Promise<OtelProbeSignal[]>;
|
|
123
136
|
log?: (msg: string) => void;
|
|
124
137
|
}
|
|
125
138
|
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
package/dist/doctor.js
CHANGED
|
@@ -17,8 +17,8 @@ import { delimiter, join } from "node:path";
|
|
|
17
17
|
import { credentialsDir } from "./credentials.js";
|
|
18
18
|
import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
|
|
19
19
|
import { classifyTokenExpiry } from "./launch.js";
|
|
20
|
-
import {
|
|
21
|
-
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
20
|
+
import { otelChildEnv, resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
21
|
+
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveTelemetryProbePath } from "./paths.js";
|
|
22
22
|
import { readActiveProfile } from "./profiles.js";
|
|
23
23
|
import { resolveMcpConfigPath } from "./mcpCommand.js";
|
|
24
24
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
@@ -201,11 +201,22 @@ export function checkGh(onPath) {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
|
|
204
|
+
* Run the extension's telemetry probe against the exact env a session would
|
|
205
|
+
* get: one real span, one metric point, one log record, each reported with
|
|
206
|
+
* the collector's actual verdict. Dynamic import so `doctor` on a machine
|
|
207
|
+
* with no collector never loads the OTel SDK graph.
|
|
208
|
+
*/
|
|
209
|
+
async function defaultProbeOtel(env) {
|
|
210
|
+
const mod = (await import(resolveTelemetryProbePath()));
|
|
211
|
+
return (await mod.probeTelemetryExport(env)).results;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Advisory OTel-export line: says whether sessions will stream telemetry to
|
|
215
|
+
* an OTLP collector, from which config source, and — when a collector is
|
|
216
|
+
* configured — whether it ACCEPTED a real record on each signal (a wrong API
|
|
217
|
+
* key, a wrong path, or a protocol mismatch shows up as the collector's own
|
|
218
|
+
* error). Never flips the exit code — most machines have no collector, and
|
|
219
|
+
* that is the healthy default.
|
|
209
220
|
*/
|
|
210
221
|
export async function checkOtelExport(config, deps = {}) {
|
|
211
222
|
if (!config) {
|
|
@@ -222,23 +233,32 @@ export async function checkOtelExport(config, deps = {}) {
|
|
|
222
233
|
? "workspace settings"
|
|
223
234
|
: ".pi/settings.json";
|
|
224
235
|
const base = `on → ${config.endpoint} (${source}, metadata-only)`;
|
|
225
|
-
const
|
|
236
|
+
const baseEnv = deps.baseEnv ?? process.env;
|
|
237
|
+
const env = { ...baseEnv, ...otelChildEnv(config, baseEnv) };
|
|
238
|
+
const doProbe = deps.probe ?? defaultProbeOtel;
|
|
226
239
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
const results = await doProbe(env);
|
|
241
|
+
const accepted = results.filter((r) => r.ok).map((r) => r.signal);
|
|
242
|
+
const failed = results.filter((r) => !r.ok);
|
|
243
|
+
const summary = results.map((r) => `${r.signal} ${r.ok ? "accepted" : "FAILED"}`).join(", ");
|
|
244
|
+
if (failed.length === 0) {
|
|
245
|
+
return {
|
|
230
246
|
name: "otel export (optional)",
|
|
231
247
|
status: "ok",
|
|
232
|
-
detail: `${base}
|
|
233
|
-
required: false,
|
|
234
|
-
}
|
|
235
|
-
: {
|
|
236
|
-
name: "otel export (optional)",
|
|
237
|
-
status: "warn",
|
|
238
|
-
detail: `${base}, endpoint not reachable`,
|
|
239
|
-
hint: "the collector is down, unreachable from this machine, or the URL is wrong — traces will not export until it answers",
|
|
248
|
+
detail: `${base}; ${summary}`,
|
|
240
249
|
required: false,
|
|
241
250
|
};
|
|
251
|
+
}
|
|
252
|
+
const reasons = failed.map((r) => `${r.signal} → ${r.url} (${r.protocol}): ${r.error ?? "rejected"}`).join("; ");
|
|
253
|
+
return {
|
|
254
|
+
name: "otel export (optional)",
|
|
255
|
+
status: "warn",
|
|
256
|
+
detail: `${base}; ${summary}`,
|
|
257
|
+
hint: `${reasons}. A 401/403 means the collector key is wrong, a 404 the path or site is wrong, ` +
|
|
258
|
+
`a connection error the collector is unreachable from this machine` +
|
|
259
|
+
(accepted.length > 0 ? `; ${accepted.join(" and ")} still export` : ""),
|
|
260
|
+
required: false,
|
|
261
|
+
};
|
|
242
262
|
}
|
|
243
263
|
catch {
|
|
244
264
|
// The probe itself errored (not "endpoint down" — the check could not
|
|
@@ -247,8 +267,8 @@ export async function checkOtelExport(config, deps = {}) {
|
|
|
247
267
|
return {
|
|
248
268
|
name: "otel export (optional)",
|
|
249
269
|
status: "warn",
|
|
250
|
-
detail: `${base},
|
|
251
|
-
hint: "the
|
|
270
|
+
detail: `${base}, export probe failed`,
|
|
271
|
+
hint: "the export probe could not run (the bundled extension is missing or the OTel SDK failed to load) — export is configured but unverified; sessions may still export normally",
|
|
252
272
|
required: false,
|
|
253
273
|
};
|
|
254
274
|
}
|
|
@@ -516,12 +536,17 @@ export async function gatherChecks(deps = {}) {
|
|
|
516
536
|
checks.push(checkStateDir(probeStateDir()));
|
|
517
537
|
checks.push(checkGh(ghOnPath()));
|
|
518
538
|
checks.push(checkMcpConfig(await probeMcp()));
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
539
|
+
const resolveOtel = deps.resolveOtel ??
|
|
540
|
+
((p) => resolveOtelLaunchWithWorkspace({
|
|
541
|
+
env: process.env,
|
|
542
|
+
cwd: process.cwd(),
|
|
543
|
+
creds: p.token ? { baseUrl: p.baseUrl, token: p.token } : null,
|
|
544
|
+
profileName: p.name,
|
|
545
|
+
}));
|
|
546
|
+
checks.push(await checkOtelExport(await resolveOtel(profile), {
|
|
547
|
+
...(deps.probeOtel ? { probe: deps.probeOtel } : {}),
|
|
548
|
+
baseEnv: process.env,
|
|
549
|
+
}));
|
|
525
550
|
return checks;
|
|
526
551
|
}
|
|
527
552
|
/**
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* pattern) so tests run against plain text. Renderer exceptions are swallowed
|
|
25
25
|
* by pi (degrading to the built-in fallback), so everything stays total.
|
|
26
26
|
*/
|
|
27
|
-
import { SettingsManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { SettingsManager, type ExtensionAPI, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
28
28
|
import type { RenderTheme } from "./subagentRender.js";
|
|
29
29
|
import { ToolRunTracker } from "./toolRuns.js";
|
|
30
30
|
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
@@ -87,6 +87,17 @@ export interface RegisterCondensedToolsDeps {
|
|
|
87
87
|
* image autoResize. Injectable so tests never touch the config dir.
|
|
88
88
|
*/
|
|
89
89
|
loadSettings?: (cwd: string) => SettingsManager;
|
|
90
|
+
/**
|
|
91
|
+
* sandbox composition: when present, the bash definition is passed
|
|
92
|
+
* through this wrap before registration — the sandbox adds its schema field
|
|
93
|
+
* (dangerouslyDisableSandbox), wrap-on-execute, and violation annotation
|
|
94
|
+
* while condensed keeps its rendering. The compose fn is built by
|
|
95
|
+
* sandbox/session.ts (makeSandboxBashComposition) and is a no-op when the
|
|
96
|
+
* sandbox is disabled. Condensed registers LAST among the bash-defining
|
|
97
|
+
* sites, so composing HERE is what makes the sandbox actually reach the
|
|
98
|
+
* model's tool calls.
|
|
99
|
+
*/
|
|
100
|
+
wrapBash?: (def: ToolDefinition) => ToolDefinition;
|
|
90
101
|
}
|
|
91
102
|
/**
|
|
92
103
|
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
@@ -291,15 +291,23 @@ export function registerCondensedTools(pi, deps = {}) {
|
|
|
291
291
|
catch {
|
|
292
292
|
toolOptions = { bash: {}, read: {} };
|
|
293
293
|
}
|
|
294
|
-
const buildDefinitions = (cwd) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
294
|
+
const buildDefinitions = (cwd) => {
|
|
295
|
+
const defs = {
|
|
296
|
+
read: createReadToolDefinition(cwd, toolOptions.read),
|
|
297
|
+
bash: createBashToolDefinition(cwd, toolOptions.bash),
|
|
298
|
+
edit: createEditToolDefinition(cwd),
|
|
299
|
+
write: createWriteToolDefinition(cwd),
|
|
300
|
+
grep: createGrepToolDefinition(cwd),
|
|
301
|
+
find: createFindToolDefinition(cwd),
|
|
302
|
+
ls: createLsToolDefinition(cwd),
|
|
303
|
+
};
|
|
304
|
+
// the sandbox wraps the bash DEFINITION (not the registration
|
|
305
|
+
// wrapper) so the cwd-delegating execute below still lands on the
|
|
306
|
+
// sandbox-aware tool for every cwd the session visits.
|
|
307
|
+
if (defs.bash && deps.wrapBash)
|
|
308
|
+
defs.bash = deps.wrapBash(defs.bash);
|
|
309
|
+
return defs;
|
|
310
|
+
};
|
|
303
311
|
const definitionsByCwd = new Map();
|
|
304
312
|
const definitionsFor = (cwd) => {
|
|
305
313
|
let defs = definitionsByCwd.get(cwd);
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { type SpendResponse } from "./costHud.js";
|
|
3
3
|
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
4
4
|
import { startMcp as defaultStartMcp } from "./mcp/startup.js";
|
|
5
|
+
import { type GuardianGateEvent } from "./permission/gate.js";
|
|
5
6
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
6
7
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
7
8
|
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
@@ -115,6 +116,15 @@ export declare function buildRunSpendUrl(baseUrl: string, runId: string): string
|
|
|
115
116
|
* Returns null on anything that doesn't look like the real shape.
|
|
116
117
|
*/
|
|
117
118
|
export declare function parseSpendResponse(data: unknown): SpendResponse | null;
|
|
119
|
+
/**
|
|
120
|
+
* Local trail for EVERY terminal gate outcome (sandbox auto-allow included),
|
|
121
|
+
* always on so e2e and support can see what the gate did even when the
|
|
122
|
+
* storage tier is off. Decision K: the always-on line carries outcome
|
|
123
|
+
* metadata ONLY — no command content. The command prefix and the exec
|
|
124
|
+
* justification are command/model content and ride the debug tier (never
|
|
125
|
+
* uploaded by /feedback) plus the opt-in storage stream.
|
|
126
|
+
*/
|
|
127
|
+
export declare function logGateOutcomeTrail(ev: GuardianGateEvent, env: NodeJS.ProcessEnv): void;
|
|
118
128
|
export declare function registerYagni(pi: ExtensionAPI, deps?: RegisterYagniDeps): Promise<void>;
|
|
119
129
|
export default function (pi: ExtensionAPI): Promise<void>;
|
|
120
130
|
export { makeAskYagniTool } from "./askYagniTool.js";
|
package/dist/extension/index.js
CHANGED
|
@@ -36,6 +36,9 @@ import { startMcp as defaultStartMcp, wireShutdown, deriveStartupConnectivityNot
|
|
|
36
36
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
37
37
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
38
38
|
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
|
|
39
|
+
import { registerSandbox } from "./sandbox/session.js";
|
|
40
|
+
import { sandboxAutoAllowDecision } from "./sandbox/bash.js";
|
|
41
|
+
import { registerTelemetry } from "./telemetry/register.js";
|
|
39
42
|
import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
|
|
40
43
|
import { loadPermissionRules } from "./permissionRules/loadConfig.js";
|
|
41
44
|
import { registerSubagents } from "./subagents.js";
|
|
@@ -102,6 +105,39 @@ export function parseSpendResponse(data) {
|
|
|
102
105
|
return null;
|
|
103
106
|
return d;
|
|
104
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Local trail for EVERY terminal gate outcome (sandbox auto-allow included),
|
|
110
|
+
* always on so e2e and support can see what the gate did even when the
|
|
111
|
+
* storage tier is off. Decision K: the always-on line carries outcome
|
|
112
|
+
* metadata ONLY — no command content. The command prefix and the exec
|
|
113
|
+
* justification are command/model content and ride the debug tier (never
|
|
114
|
+
* uploaded by /feedback) plus the opt-in storage stream.
|
|
115
|
+
*/
|
|
116
|
+
export function logGateOutcomeTrail(ev, env) {
|
|
117
|
+
logEvent({
|
|
118
|
+
source: "guardian",
|
|
119
|
+
level: "info",
|
|
120
|
+
event: "gate_outcome",
|
|
121
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
122
|
+
fields: {
|
|
123
|
+
outcome: ev.outcome,
|
|
124
|
+
mode: ev.mode,
|
|
125
|
+
consulted: ev.consulted,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
if (isDebug(env)) {
|
|
129
|
+
logEvent({
|
|
130
|
+
source: "guardian",
|
|
131
|
+
level: "debug",
|
|
132
|
+
event: "gate_outcome",
|
|
133
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
134
|
+
fields: {
|
|
135
|
+
commandPrefix: storagePrefix(ev.command),
|
|
136
|
+
...(ev.execJustification ? { execJustification: ev.execJustification } : {}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
105
141
|
export async function registerYagni(pi, deps = {}) {
|
|
106
142
|
const baseUrl = deps.baseUrl ?? resolveBaseUrl();
|
|
107
143
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -379,6 +415,18 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
379
415
|
// Mutating MCP tools join write/edit/bash in the gate policy: plan mode
|
|
380
416
|
// holds them, review mode confirms them.
|
|
381
417
|
const modeHolder = createModeHolder();
|
|
418
|
+
// OTel export (traces, metrics, log events; Claude Code parity). A no-op
|
|
419
|
+
// unless the launcher's gate set YAGNI_OTEL_EXPORT=1 — see the CLI's
|
|
420
|
+
// otel.ts for the sources and src/telemetry for the exporter.
|
|
421
|
+
const telemetry = registerTelemetry(pi, { env });
|
|
422
|
+
{
|
|
423
|
+
let previousMode = modeHolder.get();
|
|
424
|
+
modeHolder.onSet((m) => {
|
|
425
|
+
if (m !== previousMode)
|
|
426
|
+
telemetry.permissionModeChanged(previousMode, m);
|
|
427
|
+
previousMode = m;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
382
430
|
const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
|
|
383
431
|
const guardianState = makeGuardianState();
|
|
384
432
|
// Disabled by the local env override OR the workspace kill switch
|
|
@@ -454,11 +502,35 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
454
502
|
const hookRunner = evalMode ? null : makeHookRunner({ config: hooksConfig });
|
|
455
503
|
if (!evalMode)
|
|
456
504
|
registerHooks(pi, { config: hooksConfig });
|
|
505
|
+
// OS-level bash sandboxing. Registers the sandbox-aware bash tool
|
|
506
|
+
// (dangerouslyDisableSandbox param + wrap + violation annotation) and the
|
|
507
|
+
// session lifecycle (init/reset, fail-soft unless failIfUnavailable).
|
|
508
|
+
// Null on unsupported platforms — stock bash stays. Shares the loaded
|
|
509
|
+
// permission rules so sandbox config and the gate read one rule source.
|
|
510
|
+
const sandboxHandle = evalMode
|
|
511
|
+
? null
|
|
512
|
+
: registerSandbox(pi, {
|
|
513
|
+
cwd: process.cwd(),
|
|
514
|
+
env,
|
|
515
|
+
hasUI: (ctx) => ctx.hasUI,
|
|
516
|
+
// Anchors project-protected paths (.yagni-code + its config.json in
|
|
517
|
+
// denyWrite) and project-sourced permission rules to the repo the
|
|
518
|
+
// session runs in — same root the gate uses for its rule anchoring.
|
|
519
|
+
projectRoot: process.cwd(),
|
|
520
|
+
});
|
|
521
|
+
if (sandboxHandle)
|
|
522
|
+
sandboxHandle.setRules(loadedRules.rules);
|
|
457
523
|
registerPermissionGate(pi, {
|
|
524
|
+
...(sandboxHandle
|
|
525
|
+
? {
|
|
526
|
+
sandboxAutoAllow: (toolName, params) => sandboxAutoAllowDecision(toolName, params, sandboxHandle.manager, sandboxHandle.settings()),
|
|
527
|
+
}
|
|
528
|
+
: {}),
|
|
458
529
|
modeHolder,
|
|
459
530
|
guardianState,
|
|
460
531
|
guardianLimits,
|
|
461
532
|
guardianTier,
|
|
533
|
+
onToolDecision: (ev) => telemetry.toolDecision(ev),
|
|
462
534
|
...(loadedRules.rules.length > 0
|
|
463
535
|
? {
|
|
464
536
|
permissionRules: loadedRules.rules,
|
|
@@ -576,51 +648,50 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
576
648
|
// rationale. Fire-and-forget: one attempt, short timeout, failures logged
|
|
577
649
|
// fail-soft to the unified sink (source:"guardian") — a storage outage
|
|
578
650
|
// never touches the session.
|
|
579
|
-
onGuardianEvent:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
method: "POST",
|
|
602
|
-
headers: {
|
|
603
|
-
"content-type": "application/json",
|
|
604
|
-
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
605
|
-
...attributionHeaders(deps.env),
|
|
606
|
-
},
|
|
607
|
-
body: JSON.stringify(body),
|
|
608
|
-
}, {
|
|
609
|
-
fetchImpl: authedFetch,
|
|
610
|
-
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
611
|
-
});
|
|
612
|
-
if (!res.ok) {
|
|
613
|
-
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
614
|
-
}
|
|
651
|
+
onGuardianEvent: (ev) => {
|
|
652
|
+
logGateOutcomeTrail(ev, env);
|
|
653
|
+
// Opt-in storage stream (YAG-510): tier decides what leaves the machine.
|
|
654
|
+
if (guardianStorageTier === "off" || evalMode)
|
|
655
|
+
return;
|
|
656
|
+
void (async () => {
|
|
657
|
+
try {
|
|
658
|
+
const body = {
|
|
659
|
+
sessionId: env.YAGNI_SESSION_ID ?? null,
|
|
660
|
+
commandHash: createHash("sha256").update(ev.command).digest("hex"),
|
|
661
|
+
commandPrefix: storagePrefix(ev.command),
|
|
662
|
+
outcome: ev.outcome,
|
|
663
|
+
mode: ev.mode,
|
|
664
|
+
...(ev.execJustification ? { execJustification: ev.execJustification } : {}),
|
|
665
|
+
...(ev.riskLevel ? { riskLevel: ev.riskLevel } : {}),
|
|
666
|
+
...(ev.tier ? { tier: ev.tier } : {}),
|
|
667
|
+
...(ev.durationMs !== undefined ? { durationMs: ev.durationMs } : {}),
|
|
668
|
+
};
|
|
669
|
+
if (guardianStorageTier === "raw") {
|
|
670
|
+
body.command = redactCommand(ev.command);
|
|
671
|
+
if (ev.rationale)
|
|
672
|
+
body.rationale = redactCommand(ev.rationale);
|
|
615
673
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
674
|
+
const res = await resilientFetch(`${baseUrl}/api/yagni-code/guardian-events`, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: {
|
|
677
|
+
"content-type": "application/json",
|
|
678
|
+
authorization: `Bearer ${getTokenFn() ?? ""}`,
|
|
679
|
+
...attributionHeaders(deps.env),
|
|
680
|
+
},
|
|
681
|
+
body: JSON.stringify(body),
|
|
682
|
+
}, {
|
|
683
|
+
fetchImpl: authedFetch,
|
|
684
|
+
policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
|
|
685
|
+
});
|
|
686
|
+
if (!res.ok) {
|
|
687
|
+
guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
|
|
621
688
|
}
|
|
622
|
-
}
|
|
623
|
-
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
guardianLogSink({ event: "guardian_event_post_failed", kind: "network" });
|
|
692
|
+
}
|
|
693
|
+
})();
|
|
694
|
+
},
|
|
624
695
|
...(mcpMutatingSet.size > 0 || mcpOutcome.manager
|
|
625
696
|
? {
|
|
626
697
|
policy: {
|
|
@@ -852,12 +923,25 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
852
923
|
try {
|
|
853
924
|
registerCondensedTools(pi, {
|
|
854
925
|
...(scratchpadDirPath ? { scratchpadDir: scratchpadDirPath } : {}),
|
|
926
|
+
// condensed owns the FINAL bash registration (it re-registers
|
|
927
|
+
// built-ins last), so the sandbox composes INTO it here. Identity when
|
|
928
|
+
// the sandbox is off.
|
|
929
|
+
...(sandboxHandle ? { wrapBash: sandboxHandle.composeBash } : {}),
|
|
855
930
|
});
|
|
856
931
|
}
|
|
857
932
|
catch {
|
|
858
933
|
// Rendering must never break activation; pi's built-ins remain.
|
|
859
934
|
}
|
|
860
935
|
}
|
|
936
|
+
else if (sandboxHandle && env.YAGNI_CLASSIC_TOOL_ROWS !== "1") {
|
|
937
|
+
// Condensed inactive (eval mode / desktop): the sandbox's own
|
|
938
|
+
// registration is the one that reaches the tool registry. NOT under the
|
|
939
|
+
// classic-rows escape hatch — classic bash must stay pi's stock
|
|
940
|
+
// registration (the hatch's contract); a competing one would replace
|
|
941
|
+
// classic rendering with the sandbox's wrap. Desktop/eval still need
|
|
942
|
+
// the sandbox to reach bash, so the hatch is the one carve-out.
|
|
943
|
+
sandboxHandle.registerOwnBash();
|
|
944
|
+
}
|
|
861
945
|
// Own the identity + inject live company context (and repo rules) on every
|
|
862
946
|
// turn. The extension loads identically in every pi process this app spawns —
|
|
863
947
|
// the interactive driver AND every `/go` stage child, subagent, and advisor
|