agy-context-saver 2.0.0 → 2.0.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/CHANGELOG.md +12 -0
- package/README.md +3 -1
- package/bin/agy-context.mjs +7 -3
- package/lib/transcript-core.mjs +298 -76
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/execution-guard-hook.mjs +30 -1
- package/scripts/install-register.mjs +11 -4
- package/scripts/provision-rtk.mjs +9 -3
- package/scripts/release-preflight.mjs +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to `Agy-Context-Saver` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.1] - 2026-11-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Pinned `rtk v1.1.0` Release Checksums (`scripts/provision-rtk.mjs`, `tests/test-rtk-ledger-fresh-install.mjs`)**: Pinned verified SHA-256 digests from upstream `rtk v1.1.0` `checksums.txt` across all 5 supported platform archives (`win32-x64`, `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`) so clean installs without a pre-installed `rtk` binary can automatically download and verify `rtk v1.1.0` fail-closed.
|
|
12
|
+
|
|
13
|
+
### Security
|
|
14
|
+
- **Operation-Bound, Single-Invocation Capability Grants (`lib/transcript-core.mjs`, `bin/agy-context.mjs`, `scripts/execution-guard-hook.mjs`)**: Bound `verifyTranscriptCoreAccess()` to canonical subcommand (`read`, `query`, `step`, `health`, `list`) and recorded `targetConvId`, and cleared the in-memory `processVerifiedGrant` synchronously upon path resolution (before async stream I/O begins) and on any early error (`L39`) so grants cannot be reused within the same process, shared across concurrent `Promise.all` calls, or repurposed across mismatched subcommands or conversation targets.
|
|
15
|
+
- **Independent `--file` Brain-Directory Containment (`lib/transcript-core.mjs`, `scripts/execution-guard-hook.mjs`)**: Enforced independent `isContainedInDir()` checks on `--file` (`explicitFile`) inside `resolveTranscriptPath()` (`<brainDir>/<callerConvId>` in `self-only` mode and `<brainDir>` whenever any parent access policy or first-line launch restriction is active) and updated `extractConvId()` to resolve conversation folder IDs relative to `getBrainDir()` (`L38`).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Documentation & Release Hygiene (`README.md`, `SECURITY.md`, `docs/installation.md`, `docs/governance.md`, `docs/architecture.md`, `docs/plans/LEDGER.md`, `scripts/release-preflight.mjs`)**: Added a `v1.x` to `v2.0` migration note and explicit `RTK >= 1.1.0` prerequisite details to `README.md` (`L37`), clarified application-layer `PreToolUse`/CLI defense-in-depth boundaries vs. OS-kernel sandboxing and `--id` vs. `--file` containment rules (`L40`), and enforced `plugin.json` / `package.json` / `CHANGELOG.md` version parity in `scripts/release-preflight.mjs`.
|
|
19
|
+
|
|
8
20
|
## [2.0.0] - 2026-11-14
|
|
9
21
|
|
|
10
22
|
### Added
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The repository ships four components:
|
|
|
22
22
|
|
|
23
23
|
- **Node.js:** `>= 22.0.0`
|
|
24
24
|
- **Google Antigravity:** Antigravity 2.0+ (Desktop IDE or `agy` CLI)
|
|
25
|
-
- **RTK:** `>= 1.1.0`
|
|
25
|
+
- **RTK:** `>= 1.1.0` (`brew install rtk`, `cargo install --locked rtk`, or downloaded automatically into `bin/` during `agy-context setup` with pinned SHA-256 verification)
|
|
26
26
|
|
|
27
27
|
### Install and verify
|
|
28
28
|
|
|
@@ -37,6 +37,8 @@ npm run setup
|
|
|
37
37
|
agy-context status
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
> **Upgrading from v1.x:** Running `agy-context setup` automatically removes legacy `mcp_config.json` entries, sweeps stale `~/.gemini/antigravity/mcp/agy-context-saver*` schema caches, and replaces the retired v1.x MCP server with the on-demand `agy-context` CLI and updated `PreToolUse` hook.
|
|
41
|
+
|
|
40
42
|
To remove the plugin link, hook entries, and CLI shims:
|
|
41
43
|
|
|
42
44
|
```bash
|
package/bin/agy-context.mjs
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
checkContextHealth,
|
|
18
18
|
getBrainDir,
|
|
19
19
|
manageTranscriptAccessPolicy,
|
|
20
|
-
verifyTranscriptCoreAccess
|
|
20
|
+
verifyTranscriptCoreAccess,
|
|
21
|
+
consumeTranscriptAccessGrant
|
|
21
22
|
} from "../lib/transcript-core.mjs";
|
|
22
23
|
import {
|
|
23
24
|
runInstall,
|
|
@@ -165,6 +166,7 @@ function validateExplicitFileFlag(fileArg) {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
function emitError(message, asJson = false, extra = {}) {
|
|
169
|
+
consumeTranscriptAccessGrant();
|
|
168
170
|
if (asJson) {
|
|
169
171
|
console.error(JSON.stringify({ isError: true, error: message, ...extra }, null, 2));
|
|
170
172
|
} else {
|
|
@@ -213,7 +215,8 @@ async function handleTranscriptCommand(argv) {
|
|
|
213
215
|
includeToolCalls: true,
|
|
214
216
|
fullArgs: Boolean(opts["full-args"]),
|
|
215
217
|
includeThinking: Boolean(opts["include-thinking"]),
|
|
216
|
-
allowExternalPaths: false
|
|
218
|
+
allowExternalPaths: false,
|
|
219
|
+
subcommand: sub
|
|
217
220
|
});
|
|
218
221
|
if (res.isError) {
|
|
219
222
|
emitError(res.text, Boolean(opts.json), res.candidates ? { candidates: res.candidates } : {});
|
|
@@ -317,7 +320,8 @@ async function handleTranscriptCommand(argv) {
|
|
|
317
320
|
fullArgs: Boolean(opts["full-args"]),
|
|
318
321
|
field: opts.field,
|
|
319
322
|
includeThinking: Boolean(opts["include-thinking"]),
|
|
320
|
-
allowExternalPaths: false
|
|
323
|
+
allowExternalPaths: false,
|
|
324
|
+
subcommand: sub
|
|
321
325
|
});
|
|
322
326
|
|
|
323
327
|
if (res.isError) {
|
package/lib/transcript-core.mjs
CHANGED
|
@@ -61,8 +61,29 @@ export function getBrainDir() {
|
|
|
61
61
|
return process.env.AGY_BRAIN_DIR || path.join(getProfileHomeDir(), ".gemini", "antigravity", "brain");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function extractConvId(p) {
|
|
65
|
-
const
|
|
64
|
+
export function extractConvId(p, { strictBrainContainment = false } = {}) {
|
|
65
|
+
const rawStr = String(p || "");
|
|
66
|
+
try {
|
|
67
|
+
const brainRoot = realOrNull(getBrainDir()) || path.resolve(getBrainDir());
|
|
68
|
+
const targetResolved = realOrNull(rawStr) || path.resolve(rawStr);
|
|
69
|
+
const norm = (s) => (os.platform() === "win32" ? s.toLowerCase() : s);
|
|
70
|
+
const rel = path.relative(norm(brainRoot), norm(targetResolved));
|
|
71
|
+
if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
72
|
+
const origRel = path.relative(brainRoot, targetResolved);
|
|
73
|
+
const segments = origRel.split(path.sep).filter(Boolean);
|
|
74
|
+
const firstSeg = segments[0];
|
|
75
|
+
if (firstSeg && firstSeg !== "." && firstSeg !== "..") {
|
|
76
|
+
if (strictBrainContainment && segments.length === 1 && firstSeg.toLowerCase().endsWith(".jsonl")) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return firstSeg;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
if (strictBrainContainment) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const normalized = rawStr.replace(/\\/g, "/");
|
|
66
87
|
const brainMatch = normalized.match(/\/brain\/([^/]+)\//);
|
|
67
88
|
if (brainMatch) return brainMatch[1];
|
|
68
89
|
const logsMatch = normalized.match(/\/([^/]+)\/\.system_generated\/logs\//);
|
|
@@ -230,19 +251,53 @@ function hasActiveLaunchRestrictedSession() {
|
|
|
230
251
|
return false;
|
|
231
252
|
}
|
|
232
253
|
|
|
254
|
+
export function canonicalizeTranscriptSubcommand(sub) {
|
|
255
|
+
const s = String(sub || "").trim().toLowerCase();
|
|
256
|
+
if (s === "read" || s === "outline" || s === "summary") return "read";
|
|
257
|
+
if (s === "query" || s === "grep" || s === "search" || s === "find" || s === "tools") return "query";
|
|
258
|
+
if (s === "step") return "step";
|
|
259
|
+
if (s === "health" || s === "stats" || s === "info") return "health";
|
|
260
|
+
if (s === "list" || s === "ls") return "list";
|
|
261
|
+
if (s === "resolve") return "resolve";
|
|
262
|
+
return s;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function clearProcessTranscriptGrant() {
|
|
266
|
+
processVerifiedGrant = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function consumeTranscriptAccessGrant() {
|
|
270
|
+
processVerifiedGrant = null;
|
|
271
|
+
try {
|
|
272
|
+
const store = readTranscriptAccessStore();
|
|
273
|
+
if (store.lastHookGrant) {
|
|
274
|
+
store.lastHookGrant = null;
|
|
275
|
+
writeTranscriptAccessStore(store);
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
|
|
233
280
|
/**
|
|
234
281
|
* Defense-in-depth runtime verification inside transcript-core.mjs:
|
|
235
282
|
* When any parent policy restriction or active launch-directive restriction exists,
|
|
236
|
-
* verifies that PreToolUse issued a fresh (<
|
|
283
|
+
* verifies that PreToolUse issued a fresh (< 10s), operation-bound single-use capability grant.
|
|
237
284
|
*/
|
|
238
|
-
export function verifyTranscriptCoreAccess({
|
|
285
|
+
export function verifyTranscriptCoreAccess({
|
|
286
|
+
targetConvId = null,
|
|
287
|
+
subcommand = "read",
|
|
288
|
+
consumeGrant = true,
|
|
289
|
+
isImplicitCurrent = false
|
|
290
|
+
} = {}) {
|
|
239
291
|
const store = readTranscriptAccessStore();
|
|
240
292
|
const activePolicies = Object.values(store.policies || {}).filter(
|
|
241
293
|
(p) => p.mode !== "open" || (Array.isArray(p.deny) && p.deny.length > 0) || (Array.isArray(p.allow) && p.allow.length > 0)
|
|
242
294
|
);
|
|
243
295
|
const hasRestriction = activePolicies.length > 0 || hasActiveLaunchRestrictedSession();
|
|
244
296
|
if (!hasRestriction) {
|
|
245
|
-
|
|
297
|
+
if (consumeGrant) {
|
|
298
|
+
processVerifiedGrant = null;
|
|
299
|
+
}
|
|
300
|
+
return { allowed: true, mode: "open", hasRestriction: false };
|
|
246
301
|
}
|
|
247
302
|
|
|
248
303
|
let grant = null;
|
|
@@ -257,29 +312,69 @@ export function verifyTranscriptCoreAccess({ targetConvId = null, subcommand = "
|
|
|
257
312
|
}
|
|
258
313
|
|
|
259
314
|
if (!grant) {
|
|
315
|
+
processVerifiedGrant = null;
|
|
260
316
|
return {
|
|
261
317
|
allowed: false,
|
|
318
|
+
hasRestriction: true,
|
|
262
319
|
error:
|
|
263
320
|
"Antigravity Execution Governance: Transcript access is currently restricted by parent governance policy; direct or unverified transcript-core invocation outside an authorized PreToolUse CLI call is blocked."
|
|
264
321
|
};
|
|
265
322
|
}
|
|
266
323
|
|
|
324
|
+
const reqCanon = canonicalizeTranscriptSubcommand(subcommand);
|
|
325
|
+
const grantCanon = canonicalizeTranscriptSubcommand(grant.subcommand);
|
|
326
|
+
if (!reqCanon || !grantCanon || reqCanon !== grantCanon) {
|
|
327
|
+
processVerifiedGrant = null;
|
|
328
|
+
return {
|
|
329
|
+
allowed: false,
|
|
330
|
+
hasRestriction: true,
|
|
331
|
+
error: `Antigravity Execution Governance: Capability grant subcommand mismatch (grant authorized '${grant.subcommand}', but '${subcommand}' was requested).`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
267
335
|
if (grant.mode === "self-only") {
|
|
268
|
-
if (
|
|
336
|
+
if (reqCanon === "list") {
|
|
337
|
+
processVerifiedGrant = null;
|
|
269
338
|
return {
|
|
270
339
|
allowed: false,
|
|
340
|
+
hasRestriction: true,
|
|
271
341
|
error: `Antigravity Execution Governance: 'agy-context transcript ${subcommand}' is disabled in self-only mode (caller: ${grant.callerConvId}).`
|
|
272
342
|
};
|
|
273
343
|
}
|
|
274
|
-
if (targetConvId && grant.callerConvId
|
|
344
|
+
if (targetConvId !== null && (!grant.callerConvId || grant.callerConvId === "global" || targetConvId !== grant.callerConvId)) {
|
|
345
|
+
processVerifiedGrant = null;
|
|
275
346
|
return {
|
|
276
347
|
allowed: false,
|
|
348
|
+
hasRestriction: true,
|
|
277
349
|
error: `Antigravity Execution Governance: Cross-session transcript inspection of '${targetConvId}' is disabled in self-only mode (caller: ${grant.callerConvId}).`
|
|
278
350
|
};
|
|
279
351
|
}
|
|
280
352
|
}
|
|
281
353
|
|
|
282
|
-
|
|
354
|
+
if (targetConvId !== null && grant.targetConvId) {
|
|
355
|
+
const isGrantCurrent = String(grant.targetConvId).toLowerCase() === "current";
|
|
356
|
+
if (isGrantCurrent ? !isImplicitCurrent : targetConvId !== grant.targetConvId) {
|
|
357
|
+
processVerifiedGrant = null;
|
|
358
|
+
return {
|
|
359
|
+
allowed: false,
|
|
360
|
+
hasRestriction: true,
|
|
361
|
+
error: `Antigravity Execution Governance: Capability grant target mismatch (grant authorized '${grant.targetConvId}', but '${targetConvId}' was requested).`
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (consumeGrant) {
|
|
367
|
+
processVerifiedGrant = null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
allowed: true,
|
|
372
|
+
hasRestriction: true,
|
|
373
|
+
mode: grant.mode || "open",
|
|
374
|
+
callerConvId: grant.callerConvId,
|
|
375
|
+
targetConvId: grant.targetConvId || null,
|
|
376
|
+
subcommand: grant.subcommand || null
|
|
377
|
+
};
|
|
283
378
|
}
|
|
284
379
|
|
|
285
380
|
export function formatTranscriptAccessStatus(store = null) {
|
|
@@ -452,7 +547,10 @@ function isContainedInDir(targetPath, parentDir) {
|
|
|
452
547
|
const realTarget = realOrNull(targetPath);
|
|
453
548
|
const realParent = realOrNull(parentDir);
|
|
454
549
|
if (!realTarget || !realParent) return false;
|
|
455
|
-
|
|
550
|
+
const norm = (p) => (os.platform() === "win32" ? p.toLowerCase() : p);
|
|
551
|
+
const nt = norm(realTarget);
|
|
552
|
+
const np = norm(realParent);
|
|
553
|
+
return nt === np || nt.startsWith(np.endsWith(path.sep) ? np : np + path.sep);
|
|
456
554
|
}
|
|
457
555
|
|
|
458
556
|
function siblingModePath(filePath, mode) {
|
|
@@ -510,51 +608,109 @@ function scanBrainTranscripts(brainDir) {
|
|
|
510
608
|
* inside `realpath(getBrainDir())`.
|
|
511
609
|
*/
|
|
512
610
|
export function resolveTranscriptPath(target, mode = "compact", options = {}) {
|
|
513
|
-
const {
|
|
611
|
+
const {
|
|
612
|
+
explicitFile = null,
|
|
613
|
+
allowExternalPaths = false,
|
|
614
|
+
subcommand = "read",
|
|
615
|
+
retainGrantForFallback = false
|
|
616
|
+
} = options;
|
|
514
617
|
const isFull = mode === "full";
|
|
515
618
|
const fileName = isFull ? "transcript_full.jsonl" : "transcript.jsonl";
|
|
516
619
|
const brainDir = getBrainDir();
|
|
517
620
|
|
|
518
|
-
const
|
|
621
|
+
const hasExplicitFile = explicitFile !== null && explicitFile !== undefined && String(explicitFile).trim() !== "";
|
|
622
|
+
const rawTargetInput = String(target || "").trim();
|
|
623
|
+
const isImplicitCurrent = !hasExplicitFile && (!rawTargetInput || rawTargetInput.toLowerCase() === "current");
|
|
624
|
+
|
|
625
|
+
const preAccess = verifyTranscriptCoreAccess({ subcommand, consumeGrant: false, isImplicitCurrent });
|
|
519
626
|
if (!preAccess.allowed) {
|
|
520
|
-
return { error: preAccess.error };
|
|
627
|
+
return { error: preAccess.error, isGovernanceError: true };
|
|
521
628
|
}
|
|
522
629
|
|
|
630
|
+
const failResolution = (errorMsg, extra = {}) => {
|
|
631
|
+
if (!retainGrantForFallback) {
|
|
632
|
+
consumeTranscriptAccessGrant();
|
|
633
|
+
}
|
|
634
|
+
return { error: errorMsg, ...extra };
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const enforceResolvedFileContainment = (candidatePath) => {
|
|
638
|
+
if (preAccess.mode === "self-only") {
|
|
639
|
+
const selfBrainDir = path.join(brainDir, String(preAccess.callerConvId || ""));
|
|
640
|
+
if (!preAccess.callerConvId || preAccess.callerConvId === "global" || !isContainedInDir(candidatePath, selfBrainDir)) {
|
|
641
|
+
return failResolution(
|
|
642
|
+
`Antigravity Execution Governance: Cross-session '--file' access outside '${preAccess.callerConvId}' is disabled in self-only mode.`,
|
|
643
|
+
{ isGovernanceError: true }
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
} else if (preAccess.hasRestriction && !isContainedInDir(candidatePath, brainDir)) {
|
|
647
|
+
return failResolution(
|
|
648
|
+
`Security containment error: '--file' path '${candidatePath}' escapes brain directory '${brainDir}' while a transcript access policy is active.`,
|
|
649
|
+
{ isGovernanceError: true }
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
};
|
|
654
|
+
|
|
523
655
|
const finalizeResolution = (filePath, convId) => {
|
|
524
|
-
|
|
656
|
+
if (preAccess.hasRestriction && !convId) {
|
|
657
|
+
return failResolution(
|
|
658
|
+
`Security containment error: unable to derive a valid conversation directory inside '${brainDir}' for '${filePath}'.`,
|
|
659
|
+
{ isGovernanceError: true }
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
const postAccess = verifyTranscriptCoreAccess({
|
|
663
|
+
targetConvId: convId,
|
|
664
|
+
subcommand,
|
|
665
|
+
consumeGrant: !retainGrantForFallback,
|
|
666
|
+
isImplicitCurrent
|
|
667
|
+
});
|
|
525
668
|
if (!postAccess.allowed) {
|
|
526
|
-
return { error: postAccess.error };
|
|
669
|
+
return { error: postAccess.error, isGovernanceError: true };
|
|
527
670
|
}
|
|
528
671
|
return { filePath, convId };
|
|
529
672
|
};
|
|
530
673
|
|
|
531
674
|
// 0. Explicit --file operator path
|
|
532
|
-
if (
|
|
675
|
+
if (hasExplicitFile) {
|
|
533
676
|
const fileStr = String(explicitFile).trim();
|
|
534
677
|
if (!fileStr.toLowerCase().endsWith(".jsonl")) {
|
|
535
|
-
return
|
|
678
|
+
return failResolution(`Invalid --file path '${fileStr}': explicit transcript file must end with .jsonl.`);
|
|
536
679
|
}
|
|
537
680
|
const resolvedFile = path.resolve(fileStr);
|
|
538
681
|
if (!fs.existsSync(resolvedFile)) {
|
|
539
|
-
return
|
|
682
|
+
return failResolution(`Transcript file not found at --file path: '${resolvedFile}'.`);
|
|
540
683
|
}
|
|
541
684
|
const realFile = realOrNull(resolvedFile);
|
|
542
685
|
if (!realFile) {
|
|
543
|
-
return
|
|
686
|
+
return failResolution(`Unable to resolve realpath for --file path: '${resolvedFile}'.`);
|
|
544
687
|
}
|
|
545
688
|
try {
|
|
546
689
|
if (!fs.statSync(realFile).isFile()) {
|
|
547
|
-
return
|
|
690
|
+
return failResolution(`Invalid --file path '${resolvedFile}': target is not a regular file.`);
|
|
548
691
|
}
|
|
549
692
|
} catch (err) {
|
|
550
|
-
return
|
|
693
|
+
return failResolution(`Unable to stat --file path '${resolvedFile}': ${err.message}`);
|
|
551
694
|
}
|
|
552
|
-
const
|
|
553
|
-
|
|
695
|
+
const modeCandidate = siblingModePath(realFile, mode);
|
|
696
|
+
const finalPath = realOrNull(modeCandidate) || modeCandidate;
|
|
697
|
+
try {
|
|
698
|
+
if (!fs.statSync(finalPath).isFile()) {
|
|
699
|
+
return failResolution(`Invalid --file path '${finalPath}': target is not a regular file.`);
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
return failResolution(`Unable to stat --file path '${finalPath}': ${err.message}`);
|
|
703
|
+
}
|
|
704
|
+
const containmentErr = enforceResolvedFileContainment(finalPath);
|
|
705
|
+
if (containmentErr) return containmentErr;
|
|
706
|
+
return finalizeResolution(
|
|
707
|
+
finalPath,
|
|
708
|
+
extractConvId(finalPath, { strictBrainContainment: preAccess.hasRestriction })
|
|
709
|
+
);
|
|
554
710
|
}
|
|
555
711
|
|
|
556
712
|
// 1. Omitted or "current": check GEMINI_CONVERSATION_ID, else scan <brainDir> with active-swarm disambiguation
|
|
557
|
-
let raw =
|
|
713
|
+
let raw = rawTargetInput;
|
|
558
714
|
if (!raw || raw.toLowerCase() === "current") {
|
|
559
715
|
if (process.env.GEMINI_CONVERSATION_ID && process.env.GEMINI_CONVERSATION_ID.trim()) {
|
|
560
716
|
raw = process.env.GEMINI_CONVERSATION_ID.trim();
|
|
@@ -569,76 +725,93 @@ export function resolveTranscriptPath(target, mode = "compact", options = {}) {
|
|
|
569
725
|
const listing = recent
|
|
570
726
|
.map((c) => `- ${c.convId} (modified ${Math.max(0, Math.round((now - c.mtimeMs) / 1000))}s ago)`)
|
|
571
727
|
.join("\n");
|
|
572
|
-
return
|
|
573
|
-
|
|
574
|
-
candidates: recent.map((c) => c.convId)
|
|
575
|
-
|
|
728
|
+
return failResolution(
|
|
729
|
+
`Ambiguous active conversation: ${recent.length} transcripts in ${brainDir} were modified in the last 5 minutes (active swarm). Specify an explicit --id <uuid>:\n${listing}`,
|
|
730
|
+
{ candidates: recent.map((c) => c.convId) }
|
|
731
|
+
);
|
|
576
732
|
}
|
|
577
733
|
if (recent.length === 1) {
|
|
578
734
|
raw = recent[0].convId;
|
|
579
735
|
} else if (candidates.length > 0) {
|
|
580
736
|
raw = candidates[0].convId;
|
|
581
737
|
} else {
|
|
582
|
-
return
|
|
738
|
+
return failResolution(
|
|
739
|
+
`No conversation transcripts found in brain directory: ${brainDir}. Specify --id <uuid> or --file <path>.`
|
|
740
|
+
);
|
|
583
741
|
}
|
|
584
742
|
}
|
|
585
743
|
}
|
|
586
744
|
|
|
587
745
|
// 2. Reject path traversal / external path tokens in --id unless allowExternalPaths is explicitly enabled
|
|
588
746
|
const normalized = raw.replace(/\\/g, "/");
|
|
589
|
-
if (!allowExternalPaths) {
|
|
747
|
+
if (!allowExternalPaths || preAccess.hasRestriction) {
|
|
590
748
|
if (raw.includes("\0") || raw.includes("..") || raw.includes("/") || raw.includes("\\") || normalized.endsWith(".jsonl")) {
|
|
591
|
-
return
|
|
592
|
-
|
|
593
|
-
|
|
749
|
+
return failResolution(
|
|
750
|
+
`Invalid conversation ID '${raw}': must be a bare conversation UUID/folder inside ${brainDir}. Use --file <path> for explicit external .jsonl files.`
|
|
751
|
+
);
|
|
594
752
|
}
|
|
595
753
|
}
|
|
596
754
|
|
|
597
|
-
// 3. Direct file path (legacy
|
|
598
|
-
if (allowExternalPaths && normalized.endsWith(".jsonl") && fs.existsSync(normalized)) {
|
|
599
|
-
const
|
|
600
|
-
|
|
755
|
+
// 3. Direct file path (legacy compatibility when allowExternalPaths === true and no restriction is active)
|
|
756
|
+
if (allowExternalPaths && !preAccess.hasRestriction && normalized.endsWith(".jsonl") && fs.existsSync(normalized)) {
|
|
757
|
+
const modeCandidate = siblingModePath(normalized, mode);
|
|
758
|
+
const finalPath = realOrNull(modeCandidate) || modeCandidate;
|
|
759
|
+
const containmentErr = enforceResolvedFileContainment(finalPath);
|
|
760
|
+
if (containmentErr) return containmentErr;
|
|
761
|
+
return finalizeResolution(
|
|
762
|
+
finalPath,
|
|
763
|
+
extractConvId(finalPath, { strictBrainContainment: preAccess.hasRestriction })
|
|
764
|
+
);
|
|
601
765
|
}
|
|
602
766
|
|
|
603
767
|
// 4. UUID / Directory resolution under <brainDir>/<id>
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
768
|
+
const expectedSessionDir = path.join(brainDir, normalized);
|
|
769
|
+
const checkBrainCandidate = (candidatePath) => {
|
|
770
|
+
const realCandidate = realOrNull(candidatePath);
|
|
771
|
+
if (!realCandidate) {
|
|
772
|
+
return failResolution(`Unable to resolve realpath for '${candidatePath}'.`);
|
|
608
773
|
}
|
|
609
|
-
|
|
774
|
+
if ((!allowExternalPaths || preAccess.hasRestriction) && (!isContainedInDir(realCandidate, brainDir) || !isContainedInDir(realCandidate, expectedSessionDir))) {
|
|
775
|
+
return failResolution(
|
|
776
|
+
`Security containment error: resolved path '${realCandidate}' escapes conversation directory '${expectedSessionDir}'.`,
|
|
777
|
+
{ isGovernanceError: true }
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const derivedConvId = extractConvId(realCandidate, { strictBrainContainment: preAccess.hasRestriction }) || normalized;
|
|
781
|
+
return finalizeResolution(realCandidate, derivedConvId);
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const brainCandidate = path.join(expectedSessionDir, ".system_generated", "logs", fileName);
|
|
785
|
+
if (fs.existsSync(brainCandidate)) {
|
|
786
|
+
return checkBrainCandidate(brainCandidate);
|
|
610
787
|
}
|
|
611
788
|
if (isFull) {
|
|
612
|
-
const compactFallback = path.join(
|
|
789
|
+
const compactFallback = path.join(expectedSessionDir, ".system_generated", "logs", "transcript.jsonl");
|
|
613
790
|
if (fs.existsSync(compactFallback)) {
|
|
614
|
-
|
|
615
|
-
return { error: `Security containment error: resolved path '${compactFallback}' escapes brain directory '${brainDir}'.` };
|
|
616
|
-
}
|
|
617
|
-
return finalizeResolution(realOrNull(compactFallback) || compactFallback, normalized);
|
|
791
|
+
return checkBrainCandidate(compactFallback);
|
|
618
792
|
}
|
|
619
793
|
} else {
|
|
620
|
-
const fullFallback = path.join(
|
|
794
|
+
const fullFallback = path.join(expectedSessionDir, ".system_generated", "logs", "transcript_full.jsonl");
|
|
621
795
|
if (fs.existsSync(fullFallback)) {
|
|
622
|
-
|
|
623
|
-
return { error: `Security containment error: resolved path '${fullFallback}' escapes brain directory '${brainDir}'.` };
|
|
624
|
-
}
|
|
625
|
-
return finalizeResolution(realOrNull(fullFallback) || fullFallback, normalized);
|
|
796
|
+
return checkBrainCandidate(fullFallback);
|
|
626
797
|
}
|
|
627
798
|
}
|
|
628
799
|
|
|
629
|
-
// 5. Legacy local relative/absolute directory path (only when allowExternalPaths === true)
|
|
630
|
-
if (allowExternalPaths) {
|
|
800
|
+
// 5. Legacy local relative/absolute directory path (only when allowExternalPaths === true and no restriction is active)
|
|
801
|
+
if (allowExternalPaths && !preAccess.hasRestriction) {
|
|
631
802
|
const localCandidate = path.resolve(normalized, ".system_generated", "logs", fileName);
|
|
632
803
|
if (fs.existsSync(localCandidate)) {
|
|
633
|
-
|
|
804
|
+
const realLocal = realOrNull(localCandidate) || localCandidate;
|
|
805
|
+
return finalizeResolution(realLocal, extractConvId(realLocal));
|
|
634
806
|
}
|
|
635
807
|
const directCandidate = path.resolve(normalized, fileName);
|
|
636
808
|
if (fs.existsSync(directCandidate)) {
|
|
637
|
-
|
|
809
|
+
const realDirect = realOrNull(directCandidate) || directCandidate;
|
|
810
|
+
return finalizeResolution(realDirect, extractConvId(realDirect));
|
|
638
811
|
}
|
|
639
812
|
}
|
|
640
813
|
|
|
641
|
-
return
|
|
814
|
+
return failResolution(`Transcript not found for target: '${target}'. Searched: ${brainCandidate}`);
|
|
642
815
|
}
|
|
643
816
|
|
|
644
817
|
function isUrlTokenAtOffset(str, offset) {
|
|
@@ -1011,7 +1184,11 @@ export async function readTranscript({
|
|
|
1011
1184
|
includeThinking = false,
|
|
1012
1185
|
allowExternalPaths = false
|
|
1013
1186
|
} = {}) {
|
|
1014
|
-
const resolved = resolveTranscriptPath(conversationId, mode, {
|
|
1187
|
+
const resolved = resolveTranscriptPath(conversationId, mode, {
|
|
1188
|
+
explicitFile,
|
|
1189
|
+
allowExternalPaths,
|
|
1190
|
+
subcommand: "read"
|
|
1191
|
+
});
|
|
1015
1192
|
if (resolved.error) {
|
|
1016
1193
|
return { isError: true, text: resolved.error, error: resolved.error, candidates: resolved.candidates };
|
|
1017
1194
|
}
|
|
@@ -1400,19 +1577,41 @@ export async function getStepDetail({
|
|
|
1400
1577
|
} = {}) {
|
|
1401
1578
|
const parsedSelector = parseStepSelector(stepIndex, startStep, endStep);
|
|
1402
1579
|
if (parsedSelector.error) {
|
|
1580
|
+
consumeTranscriptAccessGrant();
|
|
1403
1581
|
return { isError: true, text: parsedSelector.error, error: parsedSelector.error };
|
|
1404
1582
|
}
|
|
1405
1583
|
|
|
1406
1584
|
const { indices, isMulti } = parsedSelector;
|
|
1407
1585
|
const targetSet = new Set(indices);
|
|
1408
1586
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1587
|
+
let resolvedFull;
|
|
1588
|
+
let resolvedCompact;
|
|
1589
|
+
try {
|
|
1590
|
+
resolvedFull = resolveTranscriptPath(conversationId, "full", {
|
|
1591
|
+
explicitFile,
|
|
1592
|
+
allowExternalPaths,
|
|
1593
|
+
subcommand: "step",
|
|
1594
|
+
retainGrantForFallback: true
|
|
1595
|
+
});
|
|
1596
|
+
if (resolvedFull.error && (explicitFile || resolvedFull.isGovernanceError)) {
|
|
1597
|
+
return { isError: true, text: resolvedFull.error, error: resolvedFull.error, candidates: resolvedFull.candidates };
|
|
1598
|
+
}
|
|
1599
|
+
resolvedCompact = resolveTranscriptPath(conversationId, "compact", {
|
|
1600
|
+
explicitFile,
|
|
1601
|
+
allowExternalPaths,
|
|
1602
|
+
subcommand: "step",
|
|
1603
|
+
retainGrantForFallback: false
|
|
1604
|
+
});
|
|
1605
|
+
} finally {
|
|
1606
|
+
consumeTranscriptAccessGrant();
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (resolvedCompact.error && !resolvedFull.filePath) {
|
|
1610
|
+
return { isError: true, text: resolvedCompact.error, error: resolvedCompact.error, candidates: resolvedCompact.candidates };
|
|
1412
1611
|
}
|
|
1413
1612
|
|
|
1414
|
-
let usedConvId = resolvedFull.convId;
|
|
1415
|
-
let usedFilePath = resolvedFull.filePath;
|
|
1613
|
+
let usedConvId = resolvedFull.convId || resolvedCompact.convId;
|
|
1614
|
+
let usedFilePath = resolvedFull.filePath || resolvedCompact.filePath;
|
|
1416
1615
|
const foundMap = new Map();
|
|
1417
1616
|
|
|
1418
1617
|
if (resolvedFull && resolvedFull.filePath && fs.existsSync(resolvedFull.filePath)) {
|
|
@@ -1424,17 +1623,12 @@ export async function getStepDetail({
|
|
|
1424
1623
|
|
|
1425
1624
|
if (foundMap.size < targetSet.size) {
|
|
1426
1625
|
const missingSet = new Set(indices.filter((idx) => !foundMap.has(idx)));
|
|
1427
|
-
const resolvedCompact = resolveTranscriptPath(conversationId, "compact", { explicitFile, allowExternalPaths });
|
|
1428
|
-
if (resolvedCompact.error && !resolvedFull.filePath) {
|
|
1429
|
-
return { isError: true, text: resolvedCompact.error, error: resolvedCompact.error, candidates: resolvedCompact.candidates };
|
|
1430
|
-
}
|
|
1431
1626
|
if (
|
|
1432
1627
|
resolvedCompact &&
|
|
1433
1628
|
resolvedCompact.filePath &&
|
|
1434
1629
|
resolvedCompact.filePath !== resolvedFull.filePath &&
|
|
1435
1630
|
fs.existsSync(resolvedCompact.filePath)
|
|
1436
1631
|
) {
|
|
1437
|
-
usedConvId = usedConvId || resolvedCompact.convId;
|
|
1438
1632
|
if (foundMap.size === 0) usedFilePath = resolvedCompact.filePath;
|
|
1439
1633
|
const compactMatches = await batchFindFullSteps(resolvedCompact.filePath, missingSet);
|
|
1440
1634
|
for (const [k, v] of compactMatches.entries()) {
|
|
@@ -1527,13 +1721,27 @@ export async function queryTranscript(args = {}) {
|
|
|
1527
1721
|
summaryOnly = false,
|
|
1528
1722
|
stepIndex,
|
|
1529
1723
|
field,
|
|
1530
|
-
allowExternalPaths = false
|
|
1724
|
+
allowExternalPaths = false,
|
|
1725
|
+
subcommand = "query"
|
|
1531
1726
|
} = args;
|
|
1532
1727
|
|
|
1533
|
-
|
|
1534
|
-
return getStepDetail({
|
|
1728
|
+
if (stepIndex !== undefined && stepIndex !== null) {
|
|
1729
|
+
return getStepDetail({
|
|
1730
|
+
conversationId,
|
|
1731
|
+
filePath: explicitFile,
|
|
1732
|
+
stepIndex,
|
|
1733
|
+
startStep,
|
|
1734
|
+
endStep,
|
|
1735
|
+
field,
|
|
1736
|
+
allowExternalPaths
|
|
1737
|
+
});
|
|
1535
1738
|
}
|
|
1536
1739
|
|
|
1740
|
+
const effectiveSubcommand =
|
|
1741
|
+
canonicalizeTranscriptSubcommand(subcommand) === "read" && Boolean(summaryOnly) && !query
|
|
1742
|
+
? "read"
|
|
1743
|
+
: "query";
|
|
1744
|
+
|
|
1537
1745
|
const normalizedField = field ? String(field).trim() : null;
|
|
1538
1746
|
const fieldImpliesTools = Boolean(
|
|
1539
1747
|
normalizedField === "tool_args" ||
|
|
@@ -1545,16 +1753,29 @@ export async function queryTranscript(args = {}) {
|
|
|
1545
1753
|
|
|
1546
1754
|
const resolved = resolveTranscriptPath(conversationId, mode === "full" ? "full" : "compact", {
|
|
1547
1755
|
explicitFile,
|
|
1548
|
-
allowExternalPaths
|
|
1756
|
+
allowExternalPaths,
|
|
1757
|
+
subcommand: effectiveSubcommand
|
|
1549
1758
|
});
|
|
1550
1759
|
if (resolved.error) {
|
|
1551
1760
|
return { isError: true, text: resolved.error, error: resolved.error, candidates: resolved.candidates };
|
|
1552
1761
|
}
|
|
1553
1762
|
|
|
1554
1763
|
const { filePath, convId } = resolved;
|
|
1555
|
-
const
|
|
1764
|
+
const verifiedParentDir = path.dirname(realOrNull(filePath) || filePath);
|
|
1765
|
+
const rawFullSiblingPath = filePath.endsWith("transcript_full.jsonl")
|
|
1556
1766
|
? filePath
|
|
1557
1767
|
: filePath.replace(/transcript\.jsonl$/, "transcript_full.jsonl");
|
|
1768
|
+
let fullSiblingPath = null;
|
|
1769
|
+
if (rawFullSiblingPath === filePath) {
|
|
1770
|
+
fullSiblingPath = filePath;
|
|
1771
|
+
} else if (fs.existsSync(rawFullSiblingPath)) {
|
|
1772
|
+
const realSibling = realOrNull(rawFullSiblingPath);
|
|
1773
|
+
try {
|
|
1774
|
+
if (realSibling && fs.statSync(realSibling).isFile() && isContainedInDir(realSibling, verifiedParentDir)) {
|
|
1775
|
+
fullSiblingPath = realSibling;
|
|
1776
|
+
}
|
|
1777
|
+
} catch {}
|
|
1778
|
+
}
|
|
1558
1779
|
|
|
1559
1780
|
const roleList = Array.isArray(roles)
|
|
1560
1781
|
? roles
|
|
@@ -1605,7 +1826,7 @@ export async function queryTranscript(args = {}) {
|
|
|
1605
1826
|
|
|
1606
1827
|
const flushTruncatedCandidates = async (batchIndices) => {
|
|
1607
1828
|
if (batchIndices.length === 0 || mode !== "auto") return;
|
|
1608
|
-
if (!fs.existsSync(fullSiblingPath)) return;
|
|
1829
|
+
if (!fullSiblingPath || !fs.existsSync(fullSiblingPath)) return;
|
|
1609
1830
|
const fullMap = await batchFindFullSteps(fullSiblingPath, new Set(batchIndices));
|
|
1610
1831
|
for (const stepIdx of batchIndices) {
|
|
1611
1832
|
const fullItem = fullMap.get(stepIdx);
|
|
@@ -1747,7 +1968,7 @@ export async function queryTranscript(args = {}) {
|
|
|
1747
1968
|
: mergedItems;
|
|
1748
1969
|
|
|
1749
1970
|
// Auto-dereferencing for full rendering mode (or when fullArgs / field requires untruncated tool calls)
|
|
1750
|
-
if (mode === "auto" && (!summaryOnly || effectiveFullArgs || normalizedField) && filePath !== fullSiblingPath) {
|
|
1971
|
+
if (mode === "auto" && (!summaryOnly || effectiveFullArgs || normalizedField) && fullSiblingPath && filePath !== fullSiblingPath) {
|
|
1751
1972
|
const hydrateIndices = new Set();
|
|
1752
1973
|
for (const it of finalItems) {
|
|
1753
1974
|
if (it.step_index === undefined) continue;
|
|
@@ -1918,9 +2139,10 @@ export async function checkContextHealth({
|
|
|
1918
2139
|
allowExternalPaths = false
|
|
1919
2140
|
} = {}) {
|
|
1920
2141
|
const resolved = explicitFile
|
|
1921
|
-
? resolveTranscriptPath(null, "compact", { explicitFile, allowExternalPaths })
|
|
2142
|
+
? resolveTranscriptPath(null, "compact", { explicitFile, allowExternalPaths, subcommand: "health" })
|
|
1922
2143
|
: resolveTranscriptPath(transcriptPath || conversationId || "current", "compact", {
|
|
1923
|
-
allowExternalPaths: allowExternalPaths || Boolean(transcriptPath)
|
|
2144
|
+
allowExternalPaths: allowExternalPaths || Boolean(transcriptPath),
|
|
2145
|
+
subcommand: "health"
|
|
1924
2146
|
});
|
|
1925
2147
|
|
|
1926
2148
|
if (resolved.error) {
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-context-saver",
|
|
3
3
|
"displayName": "Agy-Context-Saver",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"description": "PreToolUse execution hook, background polling governor, and transcript CLI (agy-context) for Google Antigravity, paired with RTK.",
|
|
6
6
|
"logo": "assets/logo.svg",
|
|
7
7
|
"suggestedPrompts": [
|
|
@@ -2212,13 +2212,42 @@ function checkAgyContextTranscriptCommand(segments, convId = "global") {
|
|
|
2212
2212
|
};
|
|
2213
2213
|
}
|
|
2214
2214
|
|
|
2215
|
+
let resolvedTargetConvId = targetInfo.targetId || null;
|
|
2216
|
+
if (targetInfo.explicitFile) {
|
|
2217
|
+
if (accessEval.ownerId && !accessEval.isOwner && !isPathInsideDir(targetInfo.explicitFile, getBrainDir())) {
|
|
2218
|
+
return {
|
|
2219
|
+
matched: true,
|
|
2220
|
+
allowed: false,
|
|
2221
|
+
isAccessDenial: true,
|
|
2222
|
+
reason: buildTranscriptAccessDenial({
|
|
2223
|
+
subcommand,
|
|
2224
|
+
mode: accessEval.mode,
|
|
2225
|
+
source: accessEval.source,
|
|
2226
|
+
callerConvId: convId,
|
|
2227
|
+
rosterName: accessEval.rosterName,
|
|
2228
|
+
detail: `'--file' paths outside '${getBrainDir()}' are disabled while a parent transcript access policy is active.`
|
|
2229
|
+
})
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
if (isPathInsideDir(targetInfo.explicitFile, getBrainDir())) {
|
|
2233
|
+
const rel = path.relative(
|
|
2234
|
+
tryRealpath(path.resolve(getBrainDir())),
|
|
2235
|
+
tryRealpath(path.resolve(targetInfo.explicitFile))
|
|
2236
|
+
);
|
|
2237
|
+
const firstSeg = rel.split(path.sep)[0];
|
|
2238
|
+
if (firstSeg && firstSeg !== "." && firstSeg !== "..") {
|
|
2239
|
+
resolvedTargetConvId = firstSeg;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2215
2244
|
return {
|
|
2216
2245
|
matched: true,
|
|
2217
2246
|
allowed: true,
|
|
2218
2247
|
subcommand,
|
|
2219
2248
|
mode: "open",
|
|
2220
2249
|
needsReadGrant: true,
|
|
2221
|
-
targetConvId:
|
|
2250
|
+
targetConvId: resolvedTargetConvId
|
|
2222
2251
|
};
|
|
2223
2252
|
}
|
|
2224
2253
|
|
|
@@ -23,6 +23,13 @@ import { formatTranscriptAccessStatus } from "../lib/transcript-core.mjs";
|
|
|
23
23
|
const fsPromises = fs.promises;
|
|
24
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
const repoRoot = path.resolve(__dirname, "..");
|
|
26
|
+
const PACKAGE_VERSION = (() => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || "2.0.1";
|
|
29
|
+
} catch {
|
|
30
|
+
return "2.0.1";
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
26
33
|
|
|
27
34
|
const homeDir = process.env.AGY_TEST_PROFILE_DIR || os.homedir();
|
|
28
35
|
const geminiConfigDir = path.join(homeDir, ".gemini", "config");
|
|
@@ -455,9 +462,9 @@ export async function runInstall(options = {}) {
|
|
|
455
462
|
if (existing.details.cliShim.exists) {
|
|
456
463
|
log(` [OK] CLI Shims (agy-context): Installed in ${antigravityBinDir}`);
|
|
457
464
|
}
|
|
458
|
-
log(
|
|
465
|
+
log(`\nRe-verifying and synchronizing to v${PACKAGE_VERSION}...`);
|
|
459
466
|
} else {
|
|
460
|
-
log(
|
|
467
|
+
log(`Fresh installation of Agy-Context-Saver v${PACKAGE_VERSION}...`);
|
|
461
468
|
}
|
|
462
469
|
|
|
463
470
|
// 0. Pre-flight Validation: Verify configuration directory write access
|
|
@@ -779,7 +786,7 @@ export {
|
|
|
779
786
|
|
|
780
787
|
function printCliHelp() {
|
|
781
788
|
console.log(`
|
|
782
|
-
Agy-Context-Saver Registration & Lifecycle Engine (
|
|
789
|
+
Agy-Context-Saver Registration & Lifecycle Engine (v${PACKAGE_VERSION})
|
|
783
790
|
|
|
784
791
|
Usage:
|
|
785
792
|
node scripts/install-register.mjs [command/option]
|
|
@@ -791,7 +798,7 @@ Commands & Options:
|
|
|
791
798
|
--restore-backups Restore original configuration backup files (*.bak) (standalone or during uninstall)
|
|
792
799
|
--allow-degraded Allow installation to proceed even if RTK optimizer provisioning fails
|
|
793
800
|
--help, -h, help Show this help message
|
|
794
|
-
(no arguments) Synchronize and verify
|
|
801
|
+
(no arguments) Synchronize and verify v${PACKAGE_VERSION} CLI-first installation
|
|
795
802
|
`);
|
|
796
803
|
}
|
|
797
804
|
|
|
@@ -13,9 +13,15 @@ export const RTK_VERSION = "v1.1.0";
|
|
|
13
13
|
export const MIN_RTK_VERSION = "1.1.0";
|
|
14
14
|
export const MIN_COMPATIBLE_RTK_VERSION = MIN_RTK_VERSION;
|
|
15
15
|
|
|
16
|
-
// Pinned SHA-256 checksums per release asset
|
|
17
|
-
// ensureRtkInstalled
|
|
18
|
-
export const RTK_CHECKSUMS = {
|
|
16
|
+
// Pinned SHA-256 checksums per release asset from https://github.com/rtk-ai/rtk/releases/download/v1.1.0/checksums.txt;
|
|
17
|
+
// ensureRtkInstalled verifies the downloaded archive before extraction and fails closed on any mismatch.
|
|
18
|
+
export const RTK_CHECKSUMS = {
|
|
19
|
+
"rtk-x86_64-pc-windows-msvc.zip": "c9a8f7a8bad471e64a343314168f38d2399a06fa327cdf5772fb0e645d99ea6f",
|
|
20
|
+
"rtk-aarch64-apple-darwin.tar.gz": "8da773b0954cf90b115ec95c4e4eb420f04c39de233c5db102246a6caf073ba7",
|
|
21
|
+
"rtk-x86_64-apple-darwin.tar.gz": "1e23e8b9542b58428e6409c49b1c3fb92c4c0d5125da72e80f002a46086a6c20",
|
|
22
|
+
"rtk-x86_64-unknown-linux-musl.tar.gz": "9454ad1b9489daa57ab0a9f4ebe2faaf4dc82b8a0f26c9e5135151abd7c9378d",
|
|
23
|
+
"rtk-aarch64-unknown-linux-gnu.tar.gz": "a5c00c3ab0129884bcb757d348afc1a650efa0f4f0769da97c341f28f89719ed"
|
|
24
|
+
};
|
|
19
25
|
|
|
20
26
|
export function isCompatibleRtkVersion(versionStr) {
|
|
21
27
|
if (!versionStr || typeof versionStr !== "string") return false;
|
|
@@ -44,6 +44,26 @@ if (pkg) {
|
|
|
44
44
|
errors.push(`package.json version is not a valid semver release version: ${JSON.stringify(pkg.version)}.`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
const pluginText = readText('plugin.json');
|
|
48
|
+
if (pluginText) {
|
|
49
|
+
try {
|
|
50
|
+
const pluginManifest = JSON.parse(pluginText);
|
|
51
|
+
if (pluginManifest.name !== pkg.name) {
|
|
52
|
+
errors.push(`plugin.json name ${JSON.stringify(pluginManifest.name)} does not match package.json name ${JSON.stringify(pkg.name)}.`);
|
|
53
|
+
}
|
|
54
|
+
if (pluginManifest.version !== pkg.version) {
|
|
55
|
+
errors.push(`plugin.json version ${JSON.stringify(pluginManifest.version)} does not match package.json version ${JSON.stringify(pkg.version)}.`);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
errors.push(`plugin.json is not valid JSON: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const changelogText = readText('CHANGELOG.md');
|
|
63
|
+
if (changelogText && pkg.version && !changelogText.includes(`## [${pkg.version}]`)) {
|
|
64
|
+
errors.push(`CHANGELOG.md is missing a release heading for current version "## [${pkg.version}]".`);
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
const requiredMetadata = [
|
|
48
68
|
['repository.url', pkg.repository?.url],
|
|
49
69
|
['homepage', pkg.homepage],
|