@yagni-app/code-staging 1.1.4-staging.1384.1 → 1.1.4-staging.1387.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/condensedTools.js +18 -2
- package/dist/extension/config.d.ts +14 -5
- package/dist/extension/config.js +12 -6
- package/dist/extension/fileGuards.d.ts +11 -4
- package/dist/extension/fileGuards.js +45 -33
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +16 -0
- package/package.json +2 -2
|
@@ -28,6 +28,7 @@ import * as os from "node:os";
|
|
|
28
28
|
import * as path from "node:path";
|
|
29
29
|
import { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, getAgentDir, getLanguageFromPath, highlightCode, renderDiff, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { Container, Text } from "@earendil-works/pi-tui";
|
|
31
|
+
import { logEvent } from "./errorSink.js";
|
|
31
32
|
import { ReadStateTracker, READ_FIRST_ERROR, readIsPartial, readTextOf, STALE_ERROR } from "./fileGuards.js";
|
|
32
33
|
import { isQuiet, kindForTool, ToolRunTracker } from "./toolRuns.js";
|
|
33
34
|
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
@@ -388,8 +389,23 @@ export function registerCondensedTools(pi, deps = {}) {
|
|
|
388
389
|
// nothing — its content is error text, not the file — and must not
|
|
389
390
|
// clobber a prior good record of the same path.
|
|
390
391
|
if (name === "read" && target && !isErrorResult(result)) {
|
|
391
|
-
|
|
392
|
-
|
|
392
|
+
// An image read carries an image part whose "content" is a
|
|
393
|
+
// processing note, not the file — never record it (Claude Code
|
|
394
|
+
// never stores image reads in readFileState either). The skip
|
|
395
|
+
// rides the trail so a later read-first refusal on an image-read
|
|
396
|
+
// path is diagnosable instead of confusing.
|
|
397
|
+
if (hasImages(result)) {
|
|
398
|
+
logEvent({
|
|
399
|
+
source: "condensedTools",
|
|
400
|
+
level: "info",
|
|
401
|
+
event: "read_record_skipped_image",
|
|
402
|
+
fields: {},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const partial = readIsPartial({ path: args.path, offset: args.offset, limit: args.limit }, result.details ?? undefined);
|
|
407
|
+
await readState.recordRead(target, cwd, readTextOf(result), partial);
|
|
408
|
+
}
|
|
393
409
|
}
|
|
394
410
|
else if (name === "write" && target && typeof args.content === "string" && !isErrorResult(result)) {
|
|
395
411
|
await readState.recordWrite(target, cwd, args.content);
|
|
@@ -206,12 +206,15 @@ export declare function sanitizeCallerSegment(name: string, maxLength?: number):
|
|
|
206
206
|
* Attribution headers for the boot-time /context fetch. Extends
|
|
207
207
|
* {@link attributionHeaders} (session id + caller) with the surface the
|
|
208
208
|
* session is painting for (the desktop driver sets YAGNI_SURFACE=desktop;
|
|
209
|
-
* the terminal CLI sets nothing)
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
209
|
+
* the terminal CLI sets nothing), the shipping client version, and — when
|
|
210
|
+
* known — whether the OS-level bash sandbox was active at session start
|
|
211
|
+
* ("1"/"0"; omitted when the resolved state is unknown, e.g. eval mode or an
|
|
212
|
+
* unsupported platform, so the server never reads "off" as "unknown"). The
|
|
213
|
+
* server uses these to emit its "YAGNI Code Session Started" analytics
|
|
214
|
+
* event — labels only on that side: a missing or malformed value can never
|
|
215
|
+
* fail the fetch, it just goes uncounted or unversioned.
|
|
213
216
|
*/
|
|
214
|
-
export declare function sessionTelemetryHeaders(env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
217
|
+
export declare function sessionTelemetryHeaders(env?: NodeJS.ProcessEnv, sandboxActive?: boolean): Record<string, string>;
|
|
215
218
|
/** Options for {@link fetchContextBrief}. */
|
|
216
219
|
export interface FetchContextBriefOptions {
|
|
217
220
|
baseUrl: string;
|
|
@@ -219,6 +222,12 @@ export interface FetchContextBriefOptions {
|
|
|
219
222
|
fetchImpl?: typeof fetch;
|
|
220
223
|
/** Env seam for the telemetry headers; defaults to process.env. */
|
|
221
224
|
env?: NodeJS.ProcessEnv;
|
|
225
|
+
/**
|
|
226
|
+
* Whether the OS-level bash sandbox was active at session start. Absent
|
|
227
|
+
* (undefined) when unknown — eval mode, unsupported platform, or a null
|
|
228
|
+
* sandbox handle — so the header is omitted rather than misreporting off.
|
|
229
|
+
*/
|
|
230
|
+
sandboxActive?: boolean;
|
|
222
231
|
/**
|
|
223
232
|
* The session repo (`owner/name`), when resolvable. Rides as `?repo=` so
|
|
224
233
|
* the response can carry `repoDecisionCount` for the mining beat — no
|
package/dist/extension/config.js
CHANGED
|
@@ -193,17 +193,23 @@ const CLIENT_VERSION_RE = /^[0-9a-z][0-9a-z.+-]{0,31}$/i;
|
|
|
193
193
|
* Attribution headers for the boot-time /context fetch. Extends
|
|
194
194
|
* {@link attributionHeaders} (session id + caller) with the surface the
|
|
195
195
|
* session is painting for (the desktop driver sets YAGNI_SURFACE=desktop;
|
|
196
|
-
* the terminal CLI sets nothing)
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
196
|
+
* the terminal CLI sets nothing), the shipping client version, and — when
|
|
197
|
+
* known — whether the OS-level bash sandbox was active at session start
|
|
198
|
+
* ("1"/"0"; omitted when the resolved state is unknown, e.g. eval mode or an
|
|
199
|
+
* unsupported platform, so the server never reads "off" as "unknown"). The
|
|
200
|
+
* server uses these to emit its "YAGNI Code Session Started" analytics
|
|
201
|
+
* event — labels only on that side: a missing or malformed value can never
|
|
202
|
+
* fail the fetch, it just goes uncounted or unversioned.
|
|
200
203
|
*/
|
|
201
|
-
export function sessionTelemetryHeaders(env = process.env) {
|
|
204
|
+
export function sessionTelemetryHeaders(env = process.env, sandboxActive) {
|
|
202
205
|
const headers = attributionHeaders(env);
|
|
203
206
|
headers["x-yagni-surface"] = env.YAGNI_SURFACE === "desktop" ? "desktop" : "cli";
|
|
204
207
|
const version = env.YAGNI_CODE_VERSION ?? "";
|
|
205
208
|
if (CLIENT_VERSION_RE.test(version))
|
|
206
209
|
headers["x-yagni-client-version"] = version;
|
|
210
|
+
if (sandboxActive !== undefined) {
|
|
211
|
+
headers["x-yagni-sandbox-active"] = sandboxActive ? "1" : "0";
|
|
212
|
+
}
|
|
207
213
|
return headers;
|
|
208
214
|
}
|
|
209
215
|
/**
|
|
@@ -220,7 +226,7 @@ export async function fetchContextBrief(opts) {
|
|
|
220
226
|
method: "GET",
|
|
221
227
|
headers: {
|
|
222
228
|
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
223
|
-
...sessionTelemetryHeaders(opts.env),
|
|
229
|
+
...sessionTelemetryHeaders(opts.env, opts.sandboxActive),
|
|
224
230
|
},
|
|
225
231
|
}, { fetchImpl: opts.fetchImpl });
|
|
226
232
|
if (!res.ok)
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* model has not read in this session, and refuse again when the file
|
|
10
10
|
* changed on disk since that read (mtime, with a content-compare fallback
|
|
11
11
|
* so a spurious timestamp bump — cloud sync, antivirus, `utimes` noise —
|
|
12
|
-
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
+
* follow Claude Code exactly: a create needs no prior read. ANY
|
|
14
|
+
* successful read — windowed (offset/limit) or truncated — satisfies
|
|
15
|
+
* read-first, matching Claude Code, whose Read tool never marks a
|
|
16
|
+
* partial view (its `isPartialView` exists only for auto-injected memory
|
|
17
|
+
* content that differs from disk, a path this tracker does not record).
|
|
18
|
+
* The `partial` classification instead selects the staleness strategy:
|
|
19
|
+
* a full read's record holds the whole file, so a bumped mtime can be
|
|
20
|
+
* content-compared before refusing; a partial record holds only a slice
|
|
21
|
+
* (with pi's continuation notice appended), so a bumped mtime refuses
|
|
22
|
+
* outright — Claude Code's exact stance for offset/limit reads.
|
|
16
23
|
*
|
|
17
24
|
* Fail-open contract: any guard-internal filesystem error that is not one
|
|
18
25
|
* of the two refusals (EACCES, EMFILE, a racing delete, ...) delegates
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* model has not read in this session, and refuse again when the file
|
|
10
10
|
* changed on disk since that read (mtime, with a content-compare fallback
|
|
11
11
|
* so a spurious timestamp bump — cloud sync, antivirus, `utimes` noise —
|
|
12
|
-
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* does not produce a false refusal on a full read). New-file creates
|
|
13
|
+
* follow Claude Code exactly: a create needs no prior read. ANY
|
|
14
|
+
* successful read — windowed (offset/limit) or truncated — satisfies
|
|
15
|
+
* read-first, matching Claude Code, whose Read tool never marks a
|
|
16
|
+
* partial view (its `isPartialView` exists only for auto-injected memory
|
|
17
|
+
* content that differs from disk, a path this tracker does not record).
|
|
18
|
+
* The `partial` classification instead selects the staleness strategy:
|
|
19
|
+
* a full read's record holds the whole file, so a bumped mtime can be
|
|
20
|
+
* content-compared before refusing; a partial record holds only a slice
|
|
21
|
+
* (with pi's continuation notice appended), so a bumped mtime refuses
|
|
22
|
+
* outright — Claude Code's exact stance for offset/limit reads.
|
|
16
23
|
*
|
|
17
24
|
* Fail-open contract: any guard-internal filesystem error that is not one
|
|
18
25
|
* of the two refusals (EACCES, EMFILE, a racing delete, ...) delegates
|
|
@@ -167,38 +174,43 @@ export class ReadStateTracker {
|
|
|
167
174
|
throw error; // unknown fs failure — the wrapper fails open
|
|
168
175
|
}
|
|
169
176
|
const record = this.reads.get(key);
|
|
170
|
-
if (!record
|
|
177
|
+
if (!record) {
|
|
171
178
|
throw new Error(READ_FIRST_ERROR);
|
|
172
179
|
}
|
|
173
|
-
if (Math.floor(mtimeMs)
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
180
|
+
if (Math.floor(mtimeMs) <= record.mtimeMs)
|
|
181
|
+
return; // not stale
|
|
182
|
+
// The timestamp moved. A partial record holds only a slice (plus pi's
|
|
183
|
+
// continuation notice), so it cannot be content-compared — refuse
|
|
184
|
+
// outright, Claude Code's stance for offset/limit reads.
|
|
185
|
+
if (record.partial) {
|
|
186
|
+
throw new Error(STALE_ERROR);
|
|
187
|
+
}
|
|
188
|
+
// For a full read, compare content before refusing — Claude Code's exact
|
|
189
|
+
// fallback: on some platforms (cloud sync, antivirus) the timestamp moves
|
|
190
|
+
// without content changes. The record holds the read tool's exact output
|
|
191
|
+
// and pi returns file bytes verbatim, so identical content compares equal.
|
|
192
|
+
let onDisk;
|
|
193
|
+
try {
|
|
194
|
+
onDisk = await (this.io.readFile ?? readTextFile)(key);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// Unreadable now, but the read succeeded then — stale — but the real
|
|
198
|
+
// cause (EACCES/EIO on the re-read, not an edit) rides the trail so
|
|
199
|
+
// the refusal is not misdiagnosed as a content change.
|
|
200
|
+
logEvent({
|
|
201
|
+
source: "fileGuards",
|
|
202
|
+
level: "info",
|
|
203
|
+
event: "staleness_reread_failed",
|
|
204
|
+
fields: { code: error?.code ?? "unknown" },
|
|
205
|
+
});
|
|
206
|
+
throw new Error(STALE_ERROR);
|
|
207
|
+
}
|
|
208
|
+
if (onDisk !== record.content) {
|
|
209
|
+
throw new Error(STALE_ERROR);
|
|
201
210
|
}
|
|
211
|
+
// Content-identical: refresh the record so the next write's mtime
|
|
212
|
+
// comparison uses the bumped timestamp instead of refusing again.
|
|
213
|
+
this.reads.set(key, { ...record, mtimeMs: Math.floor(mtimeMs) });
|
|
202
214
|
}
|
|
203
215
|
isInside(absolute, base) {
|
|
204
216
|
const slashed = (p) => p.replace(/\\/g, "/");
|
|
@@ -42,6 +42,7 @@ export interface RegisterYagniDeps {
|
|
|
42
42
|
getToken: () => string | undefined;
|
|
43
43
|
fetchImpl?: typeof fetch;
|
|
44
44
|
repo?: string;
|
|
45
|
+
sandboxActive?: boolean;
|
|
45
46
|
}) => Promise<ContextBrief | null>;
|
|
46
47
|
/**
|
|
47
48
|
* The session repo (`owner/name`), resolved from the origin remote before
|
package/dist/extension/index.js
CHANGED
|
@@ -1192,6 +1192,21 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1192
1192
|
}
|
|
1193
1193
|
let contextBrief;
|
|
1194
1194
|
let briefResult = null;
|
|
1195
|
+
// Session-start sandbox snapshot: a CONFIGURED-ENABLED snapshot, with the
|
|
1196
|
+
// one divergence that is knowable at factory time folded in — the
|
|
1197
|
+
// passthrough --no-sandbox flag, read from pi's own command line (pi
|
|
1198
|
+
// applies extension flag values AFTER extension load, so pi.getFlag
|
|
1199
|
+
// reads unset here; argv is the one reliable pre-session view). Omitted
|
|
1200
|
+
// (undefined) when there is no handle — eval mode or an unsupported
|
|
1201
|
+
// platform — so the telemetry header is absent rather than misreporting
|
|
1202
|
+
// off. Known remainders, both accepted: a sandbox init FAILURE at
|
|
1203
|
+
// session_start (missing deps) still reports true here (logged as
|
|
1204
|
+
// init_failed in the local sink; rare broken-deps territory), and
|
|
1205
|
+
// mid-session /sandbox toggles are out of scope — the per-call Guardian
|
|
1206
|
+
// events remain the ground truth for both.
|
|
1207
|
+
const sandboxActive = sandboxHandle && !process.argv.includes("--no-sandbox")
|
|
1208
|
+
? sandboxHandle.settings().enabled === true
|
|
1209
|
+
: undefined;
|
|
1195
1210
|
if (!evalMode) {
|
|
1196
1211
|
try {
|
|
1197
1212
|
briefResult = await fetchBrief({
|
|
@@ -1199,6 +1214,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1199
1214
|
getToken: getTokenFn,
|
|
1200
1215
|
fetchImpl: authedFetch,
|
|
1201
1216
|
repo: sessionRepo,
|
|
1217
|
+
sandboxActive,
|
|
1202
1218
|
});
|
|
1203
1219
|
contextBrief = briefResult?.brief?.trim() ? briefResult.brief : undefined;
|
|
1204
1220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.4-staging.
|
|
3
|
+
"version": "1.1.4-staging.1387.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "e8e7f5d88707ad223e9cbeb16216b80239af3224"
|
|
62
62
|
}
|