@yagni-app/code-staging 1.1.6-staging.1448.1 → 1.1.6-staging.1454.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/index.js
CHANGED
|
@@ -773,31 +773,70 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
773
773
|
// the command — its later escape attempts go to the consent dialog (the
|
|
774
774
|
// sandbox genuinely cannot run it). Content stays local: only the trimmed
|
|
775
775
|
// command string is held in memory, never logged.
|
|
776
|
-
const sandboxedFailures = new Map();
|
|
777
776
|
const SANDBOXED_FAILURES_MAX = 50;
|
|
777
|
+
/** Bounded LRU map keyed by the CANONICAL command (sandboxFailureKey): a
|
|
778
|
+
* retry differing by quote rendering or safe decoration must hit the same
|
|
779
|
+
* entry — raw-string keys would miss exactly the variable shapes the
|
|
780
|
+
* grants layer canonicalizes for. Shared by both brake maps (the boolean
|
|
781
|
+
* seed set and the escalation counter) so the refresh + eviction policy
|
|
782
|
+
* lives in ONE place — the two copies drifted once already (the counter
|
|
783
|
+
* missed the delete-then-set refresh and evicted a hot key at the cap).
|
|
784
|
+
* upsert performs the delete-then-set refresh (a re-touched key moves to
|
|
785
|
+
* the END of insertion order, so the evicted key is always the
|
|
786
|
+
* least-recently-touched one) and evicts at the cap. A null key (an
|
|
787
|
+
* uncanonicalizable command) is a no-op returning undefined. */
|
|
788
|
+
const makeSandboxFailureMap = (nextValue) => {
|
|
789
|
+
const map = new Map();
|
|
790
|
+
return {
|
|
791
|
+
has: (key) => map.has(key),
|
|
792
|
+
get: (key) => map.get(key),
|
|
793
|
+
/** Canonical-keyed upsert: refresh LRU order, compute the value from
|
|
794
|
+
* the existing one, evict at the cap. */
|
|
795
|
+
upsert: (command) => {
|
|
796
|
+
const key = sandboxFailureKey(command);
|
|
797
|
+
if (!key)
|
|
798
|
+
return undefined;
|
|
799
|
+
const value = nextValue(map.get(key), key);
|
|
800
|
+
map.delete(key);
|
|
801
|
+
map.set(key, value);
|
|
802
|
+
if (map.size > SANDBOXED_FAILURES_MAX) {
|
|
803
|
+
const oldest = map.keys().next().value;
|
|
804
|
+
if (oldest !== undefined)
|
|
805
|
+
map.delete(oldest);
|
|
806
|
+
}
|
|
807
|
+
return { key, value };
|
|
808
|
+
},
|
|
809
|
+
clear: () => { map.clear(); },
|
|
810
|
+
};
|
|
811
|
+
};
|
|
812
|
+
const sandboxedFailures = makeSandboxFailureMap(() => true);
|
|
778
813
|
const recordSandboxedFailure = (command) => {
|
|
779
|
-
|
|
780
|
-
// rendering or safe decoration must hit the same entry — raw-string
|
|
781
|
-
// keys would miss exactly the variable shapes the grants layer
|
|
782
|
-
// canonicalizes for.
|
|
783
|
-
const key = sandboxFailureKey(command);
|
|
784
|
-
if (!key)
|
|
785
|
-
return;
|
|
786
|
-
sandboxedFailures.delete(key);
|
|
787
|
-
sandboxedFailures.set(key, true);
|
|
788
|
-
if (sandboxedFailures.size > SANDBOXED_FAILURES_MAX) {
|
|
789
|
-
const oldest = sandboxedFailures.keys().next().value;
|
|
790
|
-
if (oldest !== undefined)
|
|
791
|
-
sandboxedFailures.delete(oldest);
|
|
792
|
-
}
|
|
814
|
+
sandboxedFailures.upsert(command);
|
|
793
815
|
};
|
|
794
816
|
const hasSandboxedFailure = (command) => sandboxedFailures.has(sandboxFailureKey(command));
|
|
817
|
+
// Unrecognized-failure escalation (the generic backstop behind the
|
|
818
|
+
// signature vocabulary): a sandboxed failure whose output matches NO
|
|
819
|
+
// known signature counts per canonical key; the SECOND occurrence seeds
|
|
820
|
+
// the brake. Arithmetic lands on the rung's documented contract — first
|
|
821
|
+
// failure (count 1, no seed), the escape's one silent sandboxed retry
|
|
822
|
+
// fails (count 2, seed), every further escape dialogs — for every
|
|
823
|
+
// failure shape, not just the enumerated ones. Worst case is a command
|
|
824
|
+
// failing twice for non-sandbox reasons: one dialog the user declines
|
|
825
|
+
// (the code's documented fail-safe direction). Recognized signatures
|
|
826
|
+
// still seed at 1 — the escalation never weakens the existing valve.
|
|
827
|
+
const unrecognizedSandboxFailures = makeSandboxFailureMap((existing) => (existing ?? 0) + 1);
|
|
828
|
+
const countSandboxedFailure = (command) => {
|
|
829
|
+
// The shared map's upsert refreshes LRU order, increments the count,
|
|
830
|
+
// and evicts at the cap — the same policy the seed set uses.
|
|
831
|
+
return unrecognizedSandboxFailures.upsert(command)?.value ?? 0;
|
|
832
|
+
};
|
|
795
833
|
// A mode transition is a trust-posture change — the auto-sandbox rung's
|
|
796
834
|
// prior-failure evidence clears with the gate's other session caches
|
|
797
835
|
// (the gate's own fallback map clears via modeHolder.onSet; this map is
|
|
798
836
|
// the production one, so it must clear here too).
|
|
799
837
|
modeHolder.onSet(() => {
|
|
800
838
|
sandboxedFailures.clear();
|
|
839
|
+
unrecognizedSandboxFailures.clear();
|
|
801
840
|
});
|
|
802
841
|
// settings-based permission rules, loaded once at startup (same
|
|
803
842
|
// trust-boundary posture as grants: no mid-session reload). User config
|
|
@@ -1909,18 +1948,41 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1909
1948
|
.join("\n");
|
|
1910
1949
|
if (isSandboxDenialOutput(text)) {
|
|
1911
1950
|
recordSandboxedFailure(command);
|
|
1912
|
-
// The seed must be observable (
|
|
1913
|
-
//
|
|
1914
|
-
//
|
|
1915
|
-
//
|
|
1951
|
+
// The seed must be observable (info tier — readSessionTrail filters
|
|
1952
|
+
// debug, and an invisible seed made the incident session
|
|
1953
|
+
// undebuggable; no command content, the closed signature class
|
|
1954
|
+
// rides the field): a false-positive seed session-long disables
|
|
1955
|
+
// auto-sandbox for a command, and without this line the only
|
|
1956
|
+
// symptom would be unexplained dialogs.
|
|
1916
1957
|
const signature = sandboxDenialSignature(text);
|
|
1917
1958
|
logEvent({
|
|
1918
1959
|
source: "sandbox",
|
|
1919
|
-
level: "
|
|
1960
|
+
level: "info",
|
|
1920
1961
|
event: "sandbox_failure_seeded",
|
|
1921
1962
|
fields: { signature },
|
|
1922
1963
|
});
|
|
1923
1964
|
}
|
|
1965
|
+
else {
|
|
1966
|
+
// Unrecognized-failure escalation: no known signature — count it;
|
|
1967
|
+
// the second unrecognized failure of the same canonical command
|
|
1968
|
+
// seeds the brake (the rung then dialogs instead of auto-sandboxing
|
|
1969
|
+
// into the same failure forever). Seed fires only on the 1→2
|
|
1970
|
+
// transition — a later third failure never re-seeds.
|
|
1971
|
+
const count = countSandboxedFailure(command);
|
|
1972
|
+
if (count === 2) {
|
|
1973
|
+
recordSandboxedFailure(command);
|
|
1974
|
+
logEvent({
|
|
1975
|
+
source: "sandbox",
|
|
1976
|
+
level: "info",
|
|
1977
|
+
event: "sandbox_failure_seeded",
|
|
1978
|
+
// count rides the field so the trail distinguishes the
|
|
1979
|
+
// escalation's seed (always 2) from a recognized seed (1) —
|
|
1980
|
+
// closed vocabulary only; no failure text (content stays out of
|
|
1981
|
+
// the always-on trail, the recognized branch's contract).
|
|
1982
|
+
fields: { signature: "unrecognized", count },
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1924
1986
|
}
|
|
1925
1987
|
});
|
|
1926
1988
|
// Seed the unified error trail from tool-exec failures. A tool's SUCCESS
|
|
@@ -148,9 +148,9 @@ export declare function isSandboxDenialOutput(output: string): boolean;
|
|
|
148
148
|
* offending path when found (pi-sandbox's regex, credited MIT).
|
|
149
149
|
*/
|
|
150
150
|
export declare function extractBlockedWritePath(output: string): string | null;
|
|
151
|
-
/** The
|
|
151
|
+
/** The network-posture denial classes this classifier knows (closed
|
|
152
152
|
* set — rides the sink line as a low-cardinality field). */
|
|
153
|
-
export type NetworkDenialClass = "ipc-listen" | "tls-trustd" | "loopback";
|
|
153
|
+
export type NetworkDenialClass = "ipc-listen" | "tls-trustd" | "loopback" | "dns" | "ssh-proxy";
|
|
154
154
|
/**
|
|
155
155
|
* Classify a sandbox network-posture denial from command output. Pure;
|
|
156
156
|
* advisory-only (never widens anything — the fix is a knob the USER flips,
|
|
@@ -164,6 +164,20 @@ export type NetworkDenialClass = "ipc-listen" | "tls-trustd" | "loopback";
|
|
|
164
164
|
* - loopback: EPERM/"not permitted" on a connect to 127.0.0.1/
|
|
165
165
|
* localhost/::1 (loopback bypasses the proxy via
|
|
166
166
|
* no_proxy, so the allowlist can never admit it)
|
|
167
|
+
* - dns: raw name-resolution failure (getaddrinfo/nslookup/ping
|
|
168
|
+
* spellings). The profile denies DNS by design — proxy-
|
|
169
|
+
* aware tools ride the mux where the PARENT resolves;
|
|
170
|
+
* raw resolvers fail. This class exists so the failure
|
|
171
|
+
* reads as the fence working (not the machine losing
|
|
172
|
+
* connectivity) and seeds the rung's brake — a
|
|
173
|
+
* DNS-shaped sandboxed failure re-arms the consent
|
|
174
|
+
* dialog, never loops silently.
|
|
175
|
+
* - ssh-proxy: git-over-SSH through the sandbox's SSH bridge (nc
|
|
176
|
+
* ProxyCommand): the bridge carries no credential, so
|
|
177
|
+
* the mux policy-refuses it even for granted domains;
|
|
178
|
+
* ssh renders the refusal as a dispatch fatal on
|
|
179
|
+
* "UNKNOWN port 65535" (nc's rewrite of the proxied
|
|
180
|
+
* endpoint — a real server renders its own address).
|
|
167
181
|
* Returns null when the output carries no network-posture signature — a
|
|
168
182
|
* plain file-write EPERM must NOT get a network hint.
|
|
169
183
|
*/
|
|
@@ -437,6 +437,20 @@ export function extractBlockedWritePath(output) {
|
|
|
437
437
|
* - loopback: EPERM/"not permitted" on a connect to 127.0.0.1/
|
|
438
438
|
* localhost/::1 (loopback bypasses the proxy via
|
|
439
439
|
* no_proxy, so the allowlist can never admit it)
|
|
440
|
+
* - dns: raw name-resolution failure (getaddrinfo/nslookup/ping
|
|
441
|
+
* spellings). The profile denies DNS by design — proxy-
|
|
442
|
+
* aware tools ride the mux where the PARENT resolves;
|
|
443
|
+
* raw resolvers fail. This class exists so the failure
|
|
444
|
+
* reads as the fence working (not the machine losing
|
|
445
|
+
* connectivity) and seeds the rung's brake — a
|
|
446
|
+
* DNS-shaped sandboxed failure re-arms the consent
|
|
447
|
+
* dialog, never loops silently.
|
|
448
|
+
* - ssh-proxy: git-over-SSH through the sandbox's SSH bridge (nc
|
|
449
|
+
* ProxyCommand): the bridge carries no credential, so
|
|
450
|
+
* the mux policy-refuses it even for granted domains;
|
|
451
|
+
* ssh renders the refusal as a dispatch fatal on
|
|
452
|
+
* "UNKNOWN port 65535" (nc's rewrite of the proxied
|
|
453
|
+
* endpoint — a real server renders its own address).
|
|
440
454
|
* Returns null when the output carries no network-posture signature — a
|
|
441
455
|
* plain file-write EPERM must NOT get a network hint.
|
|
442
456
|
*/
|
|
@@ -513,6 +527,52 @@ export function networkDenialHint(output) {
|
|
|
513
527
|
"The local-binding knob is ON by default — this denial means it was turned off; the user can re-enable it in the /sandbox panel (Network tab). Retrying with dangerouslyDisableSandbox is NOT needed once the knob is on.",
|
|
514
528
|
};
|
|
515
529
|
}
|
|
530
|
+
// ssh-proxy: the SSH-bridge policy refusal — matched BEFORE dns (the
|
|
531
|
+
// ordering pin): a DNS-shaped failure from an ssh prefix classifies as
|
|
532
|
+
// dns (the more honest diagnosis — alias resolution failed), while the
|
|
533
|
+
// bridge refusal is the UNKNOWN-port dispatch fatal or the mux's own
|
|
534
|
+
// refusal sentence. "UNKNOWN port 65535" is nc's rewrite of the
|
|
535
|
+
// proxied destination; a refusing-but-real server renders its own
|
|
536
|
+
// address there, so the UNKNOWN shape is proxy-specific by construction.
|
|
537
|
+
if (/ssh_dispatch_run_fatal: Connection to UNKNOWN port \d+: (Broken pipe|Connection reset)/i.test(output) ||
|
|
538
|
+
/kex_exchange_identification: Connection (?:closed|reset) by UNKNOWN port \d+/i.test(output) ||
|
|
539
|
+
/This proxy requires authentication[^.]*\bdid not offer an authentication method/i.test(output) ||
|
|
540
|
+
/SSH-2\.0-policy_refusal/.test(output)) {
|
|
541
|
+
return {
|
|
542
|
+
cls: "ssh-proxy",
|
|
543
|
+
hint: "This looks like a sandbox git-over-SSH denial (the SSH bridge routes ssh through the sandbox proxy, which carries no credential — git over SSH cannot ride it even for granted domains). " +
|
|
544
|
+
"Use the HTTPS remote for this operation, or retry outside the sandbox (the user will be asked). Do not treat this as a network outage.",
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
// dns: raw name-resolution failure — the fence denies DNS by design;
|
|
548
|
+
// proxy-aware tools ride the mux (parent-side resolution), raw
|
|
549
|
+
// resolvers do not. Per-line discipline matches the EPERM branch
|
|
550
|
+
// (numbered grep hits, quoted text, and prose-initial lines are data):
|
|
551
|
+
// "Could not resolve host" is prose-likely enough that a bare
|
|
552
|
+
// substring match would fire on quoted log lines.
|
|
553
|
+
{
|
|
554
|
+
const dnsPhrases = /Could not resolve host(?:name)?|nodename nor servname provided|Name or service not known|Temporary failure in name resolution|getaddrinfo (?:ENOTFOUND|EAI_AGAIN|EAI_NONAME)|cannot resolve \S+: Unknown host|no servers could be reached|no DNS configuration/i;
|
|
555
|
+
for (const line of output.split("\n")) {
|
|
556
|
+
const t = line.trim();
|
|
557
|
+
if (!dnsPhrases.test(t))
|
|
558
|
+
continue;
|
|
559
|
+
if (/^\d+:/.test(t))
|
|
560
|
+
continue; // numbered grep/log hit — data
|
|
561
|
+
if (/^["']/.test(t))
|
|
562
|
+
continue; // quoted text — data
|
|
563
|
+
// Prose-initial: the phrase behind an article/conjunction opener is
|
|
564
|
+
// data ("if the host could not resolve…"). A `tool:` prefix or the
|
|
565
|
+
// bare phrase at line head is the diagnostic shape (same split as
|
|
566
|
+
// the EPERM branch's prose exclusion).
|
|
567
|
+
if (/^(the|a|an|this|that|it|when|if|note|tip|for|some)\b/i.test(t))
|
|
568
|
+
continue;
|
|
569
|
+
return {
|
|
570
|
+
cls: "dns",
|
|
571
|
+
hint: "This looks like a sandbox DNS-resolution denial (raw name resolution is unavailable inside the sandbox by design — proxy-aware tools ride the network proxy, where the parent resolves). " +
|
|
572
|
+
"This is NOT evidence the machine lost connectivity; do not diagnose network health from inside the sandbox. If this command genuinely needs direct network, retry with dangerouslyDisableSandbox (the user will be asked).",
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
516
576
|
return null;
|
|
517
577
|
}
|
|
518
578
|
/**
|
|
@@ -128,9 +128,15 @@ export function makeBashComposition(manager, settings, cwd, onShellResolutionRet
|
|
|
128
128
|
// auto-allow, and the escape rung at once. Stated here so the model
|
|
129
129
|
// never mints one.
|
|
130
130
|
const envPrefixNote = "Do not prepend environment variables you have not verified exist (grep the tool's binary/docs/help first) — an unverified prefix defeats grants, auto-allow, and the sandbox retry rung; write the plain command instead.";
|
|
131
|
+
// The git-over-SSH caveat: the SSH bridge carries no credential, so a
|
|
132
|
+
// granted domain does NOT make git-over-SSH work — the model that
|
|
133
|
+
// expects the grant to cover an SSH remote reads the bridge refusal as
|
|
134
|
+
// a network outage and probes (the incident session burned 10 minutes
|
|
135
|
+
// on exactly that). The HTTPS remote is the in-sandbox cure.
|
|
136
|
+
const gitSshNote = "Git commands that talk to a remote over SSH (git@… URLs) cannot ride the sandbox's network proxy on macOS — expect an SSH-bridge or DNS failure and either use the HTTPS remote, or retry outside the sandbox (the user will be asked). A granted domain does not make git-over-SSH work.";
|
|
131
137
|
const wrapped = {
|
|
132
138
|
...def,
|
|
133
|
-
description: `${def.description}\n\n## Command sandbox\nCommands run inside an OS sandbox: ${restrictions.join("; ")}. ${strictNote}\n${heredocNote}\n${tmpdirNote}\n${envPrefixNote}`,
|
|
139
|
+
description: `${def.description}\n\n## Command sandbox\nCommands run inside an OS sandbox: ${restrictions.join("; ")}. ${strictNote}\n${heredocNote}\n${tmpdirNote}\n${envPrefixNote}\n${gitSshNote}`,
|
|
134
140
|
parameters: schema,
|
|
135
141
|
async execute(id, params, signal, onUpdate, ctx) {
|
|
136
142
|
const input = params;
|
|
@@ -358,10 +364,14 @@ export function registerSandbox(pi, opts) {
|
|
|
358
364
|
if (sessionDomainGrants.has(host))
|
|
359
365
|
return true;
|
|
360
366
|
const ui = uiBridge;
|
|
361
|
-
if (!ui?.hasUI)
|
|
367
|
+
if (!ui?.hasUI) {
|
|
368
|
+
logNetworkAsk(host, "headless", null);
|
|
362
369
|
return false; // headless: fail closed (never grant silently)
|
|
363
|
-
|
|
370
|
+
}
|
|
371
|
+
if (ui.signal?.aborted) {
|
|
372
|
+
logNetworkAsk(host, "aborted", null);
|
|
364
373
|
return false;
|
|
374
|
+
}
|
|
365
375
|
// Claude-parity grant options. Session grant is memory-only and
|
|
366
376
|
// agent-invisible; project/user write the sandbox config file.
|
|
367
377
|
const dialogOpts = {
|
|
@@ -378,13 +388,36 @@ export function registerSandbox(pi, opts) {
|
|
|
378
388
|
"Block",
|
|
379
389
|
], dialogOpts);
|
|
380
390
|
}
|
|
381
|
-
catch {
|
|
391
|
+
catch (err) {
|
|
392
|
+
// A real 600s timeout vs an immediate UI-bridge error are different
|
|
393
|
+
// failures; the deny behavior is identical, but the trail says which:
|
|
394
|
+
// the dialog's own timeout throws a name-matched abort/timeout error
|
|
395
|
+
// (pi's select timeout shape), anything else is a bridge failure.
|
|
396
|
+
const isTimeout = err instanceof Error && /timeout|abort/i.test(err.name);
|
|
397
|
+
logNetworkAsk(host, isTimeout ? "timeout" : "error", null);
|
|
382
398
|
return false; // dialog error / timeout: never grant
|
|
383
399
|
}
|
|
384
|
-
|
|
400
|
+
// A signal abort while the dialog was open resolves select to undefined
|
|
401
|
+
// — that is the session tearing down, not the user choosing Block.
|
|
402
|
+
if (ui.signal?.aborted) {
|
|
403
|
+
logNetworkAsk(host, "aborted", null);
|
|
385
404
|
return false;
|
|
386
|
-
|
|
387
|
-
if (choice
|
|
405
|
+
}
|
|
406
|
+
if (choice === undefined || choice === "Block") {
|
|
407
|
+
logNetworkAsk(host, "block", null);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
// FAIL-CLOSED choice dispatch: every branch matches its option label
|
|
411
|
+
// explicitly — an unrecognized choice (a renamed/added dialog option in
|
|
412
|
+
// a future version) never silently grants; it denies with its own
|
|
413
|
+
// outcome in the trail.
|
|
414
|
+
if (choice.startsWith("Allow for this session")) {
|
|
415
|
+
sessionDomainGrants.add(host);
|
|
416
|
+
logNetworkAsk(host, "session", null);
|
|
417
|
+
}
|
|
418
|
+
else if (choice.startsWith("Allow for this project")) {
|
|
419
|
+
sessionDomainGrants.add(host);
|
|
420
|
+
logNetworkAsk(host, "allow", "project");
|
|
388
421
|
try {
|
|
389
422
|
persistDomainGrant(opts.projectRoot ?? opts.cwd, host, resolvedStateHome);
|
|
390
423
|
}
|
|
@@ -397,6 +430,8 @@ export function registerSandbox(pi, opts) {
|
|
|
397
430
|
}
|
|
398
431
|
}
|
|
399
432
|
else if (choice.startsWith("Allow for all projects")) {
|
|
433
|
+
sessionDomainGrants.add(host);
|
|
434
|
+
logNetworkAsk(host, "allow", "user");
|
|
400
435
|
try {
|
|
401
436
|
persistDomainGrant(null, host, resolvedStateHome);
|
|
402
437
|
}
|
|
@@ -405,6 +440,10 @@ export function registerSandbox(pi, opts) {
|
|
|
405
440
|
ui.ui.notify?.("Could not persist the domain grant to the config file — it applies to this session only.", "warning");
|
|
406
441
|
}
|
|
407
442
|
}
|
|
443
|
+
else {
|
|
444
|
+
logNetworkAsk(host, "unknown", null);
|
|
445
|
+
return false; // an unrecognized choice never grants
|
|
446
|
+
}
|
|
408
447
|
// The grant must reach the live srt config before the retry.
|
|
409
448
|
manager.refreshConfig([...effectiveRulesForSandbox()]);
|
|
410
449
|
return true;
|
|
@@ -945,6 +984,23 @@ function logFsDenialHint(fs, surface) {
|
|
|
945
984
|
return;
|
|
946
985
|
logEvent({ source: "sandbox", level: "info", event: "fs_denial_hint", fields: { class: fs.cls, surface } });
|
|
947
986
|
}
|
|
987
|
+
/** Sink line for every network-ask resolution. The ask was invisible in the
|
|
988
|
+
* incident session's trail — whether the dialog fired, which option was
|
|
989
|
+
* picked, and where the grant persisted had to be reconstructed from the
|
|
990
|
+
* config file by hand. outcome: session | allow | block | timeout |
|
|
991
|
+
* aborted | headless | unknown | error (the closed resolution vocabulary);
|
|
992
|
+
* tier: where a persistent grant landed (project | user) or null. The
|
|
993
|
+
* host rides the field (same exposure class as logGrantPersistFailure's
|
|
994
|
+
* `grant`) — the sink scrubs each line, and the transcript carries hosts
|
|
995
|
+
* anyway. */
|
|
996
|
+
function logNetworkAsk(host, outcome, tier) {
|
|
997
|
+
logEvent({
|
|
998
|
+
source: "sandbox",
|
|
999
|
+
level: "info",
|
|
1000
|
+
event: "network_ask",
|
|
1001
|
+
fields: { host, outcome, tier },
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
948
1004
|
/** Sink line for a failed domain-grant persist — the thrown message is
|
|
949
1005
|
* mutateConfigJson's own path/reason text (diagnosable, no file content),
|
|
950
1006
|
* same posture as sandbox_persist_failed / rule_save_failed. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.6-staging.
|
|
3
|
+
"version": "1.1.6-staging.1454.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": "d2f1a9de6f763b2144382fb42fed7cf36efa431a"
|
|
62
62
|
}
|