@akagilnc/pi-workflow-roles 0.1.3545 → 0.1.3552
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/grok/production-host.js +36 -430
- package/dist/public-cli/main.js +60 -46
- package/dist/public-cli/registry.js +2 -1
- package/package.json +1 -1
- package/src/doctor-evidence.ts +1 -8
- package/src/grok/production-host.ts +13 -216
- package/src/grok/role-turn-host.ts +4 -358
- package/src/host-transition-prior-native.ts +23 -32
- package/src/public-cli/cli.ts +33 -4
- package/src/public-cli/countersign-run.ts +1 -0
- package/src/public-cli/inspector-run.ts +1 -0
- package/src/public-cli/notary-run.ts +1 -0
- package/src/public-cli/option-definitions.ts +11 -0
- package/src/public-cli/post-admission.ts +5 -1
- package/src/public-cli/registry.ts +1 -0
- package/src/public-cli/seat-ticket-binding.ts +6 -2
- package/src/grok/bash-seatbelt.ts +0 -69
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { copyFile, lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, relative as pathRelative } from "node:path";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
4
2
|
import { createInterface } from "node:readline";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
3
|
|
|
7
4
|
import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
|
|
8
5
|
import { renderAgentStartMaterials } from "../agent-start-materials.ts";
|
|
9
|
-
import { installGrokPreToolUseDeny } from "./bash-seatbelt.ts";
|
|
10
6
|
|
|
11
7
|
/** ACP v1 surface used by the Grok adapter. Protocol details stay in this module. */
|
|
12
8
|
export interface GrokAcpConnection {
|
|
@@ -17,13 +13,6 @@ export interface GrokAcpConnection {
|
|
|
17
13
|
close(): Promise<void>;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
export type GrokControlledInspection = Readonly<{
|
|
21
|
-
/** Active configuration whose source is neither Grok builtin nor AK injection. */
|
|
22
|
-
privateActive: readonly string[];
|
|
23
|
-
/** Active AK-owned configuration observed by the same first-party inspect call. */
|
|
24
|
-
akActive: readonly string[];
|
|
25
|
-
}>;
|
|
26
|
-
|
|
27
16
|
/** The shared envelope, prepared before session/new (systemPrompt delivery) and
|
|
28
17
|
* able to observe the host's real builtin tool surface once it arrives post-session. */
|
|
29
18
|
export type GrokPreparedTurn = Readonly<{
|
|
@@ -63,11 +52,6 @@ export function renderGrokSystemPromptOverride(authority: {
|
|
|
63
52
|
return renderAgentStartMaterials(authority.body, authority.materials);
|
|
64
53
|
}
|
|
65
54
|
|
|
66
|
-
export type GrokCapabilityDeclaration = Readonly<{
|
|
67
|
-
nativeToolNarrowing: false;
|
|
68
|
-
preToolUseDeny: boolean;
|
|
69
|
-
}>;
|
|
70
|
-
|
|
71
55
|
export type GrokSessionIdentityAuthority = Readonly<{
|
|
72
56
|
load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
|
|
73
57
|
bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
|
|
@@ -78,9 +62,7 @@ export type GrokSessionIdentityAuthority = Readonly<{
|
|
|
78
62
|
export type GrokRoleTurnHostConfig = Readonly<{
|
|
79
63
|
sessionIdentity: GrokSessionIdentityAuthority;
|
|
80
64
|
connect(request: RoleTurnRequest): Promise<GrokAcpConnection>;
|
|
81
|
-
inspect(request: RoleTurnRequest): Promise<GrokControlledInspection>;
|
|
82
65
|
prepare(request: RoleTurnRequest): Promise<GrokPreparedTurn>;
|
|
83
|
-
recordCapabilities(request: RoleTurnRequest, declaration: GrokCapabilityDeclaration): void | Promise<void>;
|
|
84
66
|
}>;
|
|
85
67
|
|
|
86
68
|
function failure(cause: "activation" | "session" | "output", name: string, code: string, details?: Readonly<Record<string, unknown>>): RoleTurnResult {
|
|
@@ -223,21 +205,13 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
|
|
|
223
205
|
return {
|
|
224
206
|
executeTurn(request) {
|
|
225
207
|
const execution = serial.then(async (): Promise<RoleTurnResult> => {
|
|
226
|
-
const inspected = await config.inspect(request);
|
|
227
|
-
if (inspected.privateActive.length !== 0) {
|
|
228
|
-
return failure("activation", "UncontrolledGrokSession", "private-config-active", {
|
|
229
|
-
privateActive: [...inspected.privateActive],
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
208
|
const continuation = request.continuation;
|
|
233
209
|
const prepared = await config.prepare(request);
|
|
234
210
|
let connection: GrokAcpConnection | undefined;
|
|
235
211
|
let sessionId: string | undefined;
|
|
236
212
|
let accepted = false;
|
|
237
213
|
try {
|
|
238
|
-
// AK injection proof is prepared MCP composition (envelope)
|
|
239
|
-
// Inspect only classifies first-party already-active sources; external packageRoot
|
|
240
|
-
// materials reach session/new via prepare, not via Grok-native inspect paths.
|
|
214
|
+
// AK injection proof is prepared MCP composition (envelope).
|
|
241
215
|
if (prepared.mcpServers.length === 0) {
|
|
242
216
|
return failure("activation", "UncontrolledGrokSession", "ak-config-missing");
|
|
243
217
|
}
|
|
@@ -249,20 +223,6 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
|
|
|
249
223
|
const initializeMeta = initialized._meta as {
|
|
250
224
|
modelState?: { availableModels?: unknown };
|
|
251
225
|
} | undefined;
|
|
252
|
-
const hookMeta = initialized._meta as { "x.ai/hooks"?: { blockingEvents?: unknown; decisions?: unknown } } | undefined;
|
|
253
|
-
const hookCapability = hookMeta?.["x.ai/hooks"];
|
|
254
|
-
const canDeny = Array.isArray(hookCapability?.blockingEvents)
|
|
255
|
-
&& hookCapability.blockingEvents.includes("pre_tool_use")
|
|
256
|
-
&& Array.isArray(hookCapability.decisions)
|
|
257
|
-
&& hookCapability.decisions.includes("deny");
|
|
258
|
-
// ADR 0008: seatbelt hangs only on the activated Fixer. Capability alone
|
|
259
|
-
// is not an installed belt, and review seats (ADR 0064) must stay unnarrowed.
|
|
260
|
-
let preToolUseDeny = false;
|
|
261
|
-
if (canDeny && request.activation.role === "fixer") {
|
|
262
|
-
await installGrokPreToolUseDeny(request.home);
|
|
263
|
-
preToolUseDeny = true;
|
|
264
|
-
}
|
|
265
|
-
await config.recordCapabilities(request, { nativeToolNarrowing: false, preToolUseDeny });
|
|
266
226
|
const modelState = initializeMeta?.modelState;
|
|
267
227
|
const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : undefined;
|
|
268
228
|
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
@@ -436,325 +396,11 @@ const PRIVATE_COMPAT_ENV = Object.fromEntries(
|
|
|
436
396
|
[`GROK_${vendor}_${kind}_ENABLED`, "false"] as const)),
|
|
437
397
|
);
|
|
438
398
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
readonly path?: unknown;
|
|
442
|
-
readonly disabled?: unknown;
|
|
443
|
-
readonly enabled?: unknown;
|
|
444
|
-
readonly compatibilityStatus?: unknown;
|
|
445
|
-
readonly source?: { readonly type?: unknown; readonly path?: unknown };
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
export type GrokInspectionClassificationOptions = Readonly<{
|
|
449
|
-
/**
|
|
450
|
-
* Calling-repo projectInstructions whose path is carried by HEAD and whose
|
|
451
|
-
* working-tree bytes match that blob (#521 repo-instructions-are-shared-material).
|
|
452
|
-
* These are shared repo material — not privateActive, not AK package injection.
|
|
453
|
-
*/
|
|
454
|
-
readonly headMatchedProjectInstructionPaths?: ReadonlySet<string>;
|
|
455
|
-
}>;
|
|
456
|
-
|
|
457
|
-
const execFileAsync = promisify(execFile);
|
|
458
|
-
|
|
459
|
-
function errnoCode(error: unknown): string | undefined {
|
|
460
|
-
if (typeof error !== "object" || error === null || !("code" in error)) return undefined;
|
|
461
|
-
const code = (error as { code: unknown }).code;
|
|
462
|
-
return typeof code === "string" ? code : undefined;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async function realpathIfPresent(path: string): Promise<string | undefined> {
|
|
466
|
-
try {
|
|
467
|
-
return await realpath(path);
|
|
468
|
-
} catch (error) {
|
|
469
|
-
if (errnoCode(error) === "ENOENT") return undefined;
|
|
470
|
-
throw error;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Typed worktree readability: ENOENT → absent; permission and other IO stay loud.
|
|
476
|
-
* Does not consult Git diagnostics.
|
|
477
|
-
*/
|
|
478
|
-
async function worktreeFilePresence(path: string): Promise<"absent" | "present"> {
|
|
479
|
-
try {
|
|
480
|
-
const handle = await open(path, "r");
|
|
481
|
-
await handle.close();
|
|
482
|
-
return "present";
|
|
483
|
-
} catch (error) {
|
|
484
|
-
if (errnoCode(error) === "ENOENT") return "absent";
|
|
485
|
-
throw error;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Map a worktree-relative path to the unique HEAD tree path it names.
|
|
491
|
-
* Exact match first; otherwise a single case-insensitive hit in the same
|
|
492
|
-
* directory (Grok may report `Claude.md` while HEAD stores `CLAUDE.md`).
|
|
493
|
-
* Path identity keeps the inspect leaf name (final symlink not followed).
|
|
494
|
-
* Git/IO failures propagate; only "not in HEAD" returns undefined.
|
|
495
|
-
*/
|
|
496
|
-
async function resolveHeadTreePath(topLevel: string, relativePath: string): Promise<string | undefined> {
|
|
497
|
-
const { stdout: exactOut } = await execFileAsync(
|
|
498
|
-
"git",
|
|
499
|
-
["ls-tree", "--name-only", "HEAD", "--", relativePath],
|
|
500
|
-
{ cwd: topLevel, encoding: "utf8" },
|
|
501
|
-
);
|
|
502
|
-
const exactHits = exactOut.split("\n").map((name) => name.trim()).filter((name) => name !== "");
|
|
503
|
-
if (exactHits.includes(relativePath)) return relativePath;
|
|
504
|
-
|
|
505
|
-
const parent = dirname(relativePath);
|
|
506
|
-
const leaf = basename(relativePath);
|
|
507
|
-
// Path absence is an empty structured ls-tree result (exit 0), never stderr prose.
|
|
508
|
-
if (parent !== ".") {
|
|
509
|
-
const { stdout: parentOut } = await execFileAsync(
|
|
510
|
-
"git",
|
|
511
|
-
["ls-tree", "--name-only", "HEAD", "--", parent],
|
|
512
|
-
{ cwd: topLevel, encoding: "utf8" },
|
|
513
|
-
);
|
|
514
|
-
const parentHits = parentOut.split("\n").map((name) => name.trim()).filter((name) => name !== "");
|
|
515
|
-
if (!parentHits.includes(parent)) return undefined;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Parent confirmed present (or root): list children. Any failure stays loud infrastructure.
|
|
519
|
-
const { stdout: listing } = parent === "."
|
|
520
|
-
? await execFileAsync("git", ["ls-tree", "--name-only", "HEAD"], {
|
|
521
|
-
cwd: topLevel,
|
|
522
|
-
encoding: "utf8",
|
|
523
|
-
})
|
|
524
|
-
: await execFileAsync("git", ["ls-tree", "--name-only", `HEAD:${parent}`], {
|
|
525
|
-
cwd: topLevel,
|
|
526
|
-
encoding: "utf8",
|
|
527
|
-
});
|
|
528
|
-
const needle = leaf.toLowerCase();
|
|
529
|
-
const hits = listing
|
|
530
|
-
.split("\n")
|
|
531
|
-
.map((name) => name.trim())
|
|
532
|
-
.filter((name) => name !== "" && basename(name).toLowerCase() === needle)
|
|
533
|
-
.map((name) => (parent === "." ? basename(name) : join(parent, basename(name))));
|
|
534
|
-
return hits.length === 1 ? hits[0] : undefined;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* True when inspect-reported path is carried by calling-repo HEAD and the bytes
|
|
539
|
-
* a host reads through that path match the HEAD blob
|
|
540
|
-
* (#521 repo-instructions-are-shared-material).
|
|
541
|
-
*
|
|
542
|
-
* Expected negatives (return false): empty/outside path, HEAD does not carry
|
|
543
|
-
* the path, worktree absent, or worktree bytes ≠ HEAD blob.
|
|
544
|
-
* Infrastructure (throw with cause): git unavailable, unexpected git/repo
|
|
545
|
-
* failure, permission or other IO on realpath/hash.
|
|
546
|
-
*/
|
|
547
|
-
export async function isHeadMatchedProjectInstruction(
|
|
548
|
-
repositoryCwd: string,
|
|
549
|
-
absolutePath: string,
|
|
550
|
-
): Promise<boolean> {
|
|
551
|
-
if (absolutePath === "" || absolutePath.includes("\0")) return false;
|
|
552
|
-
|
|
553
|
-
const { stdout: topLevelOut } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
554
|
-
cwd: repositoryCwd,
|
|
555
|
-
encoding: "utf8",
|
|
556
|
-
});
|
|
557
|
-
// Prove HEAD is readable before path negatives — corrupt/missing HEAD stays loud.
|
|
558
|
-
await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], {
|
|
559
|
-
cwd: repositoryCwd,
|
|
560
|
-
encoding: "utf8",
|
|
561
|
-
});
|
|
562
|
-
const topLevel = await realpath(topLevelOut.trim());
|
|
563
|
-
|
|
564
|
-
// Keep final leaf identity (do not realpath through a final-component symlink).
|
|
565
|
-
const parent = await realpathIfPresent(dirname(absolutePath));
|
|
566
|
-
if (parent === undefined) return false;
|
|
567
|
-
const leaf = basename(absolutePath);
|
|
568
|
-
if (leaf === "" || leaf === "." || leaf === "..") return false;
|
|
569
|
-
const candidate = join(parent, leaf);
|
|
570
|
-
const relative = pathRelative(topLevel, candidate);
|
|
571
|
-
if (relative === "" || relative.startsWith("..") || isAbsolute(relative) || relative.includes("\0")) {
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const headRel = await resolveHeadTreePath(topLevel, relative);
|
|
576
|
-
if (headRel === undefined) return false;
|
|
577
|
-
const headFile = join(topLevel, headRel);
|
|
578
|
-
|
|
579
|
-
const { stdout: headBlobOut } = await execFileAsync(
|
|
580
|
-
"git",
|
|
581
|
-
["rev-parse", "--verify", `HEAD:${headRel}`],
|
|
582
|
-
{ cwd: topLevel, encoding: "utf8" },
|
|
583
|
-
);
|
|
584
|
-
const headBlob = headBlobOut.trim();
|
|
585
|
-
|
|
586
|
-
// Prefer inspect-reported path bytes (hash-object follows symlink content).
|
|
587
|
-
// Worktree absence is typed FS ENOENT; permission and other IO stay loud.
|
|
588
|
-
// When exact casing is absent, fall back to the unique HEAD-cased path.
|
|
589
|
-
let hashTarget = candidate;
|
|
590
|
-
const candidatePresence = await worktreeFilePresence(candidate);
|
|
591
|
-
if (candidatePresence === "absent") {
|
|
592
|
-
if (candidate === headFile) return false;
|
|
593
|
-
const headPresence = await worktreeFilePresence(headFile);
|
|
594
|
-
if (headPresence === "absent") return false;
|
|
595
|
-
hashTarget = headFile;
|
|
596
|
-
}
|
|
597
|
-
const { stdout } = await execFileAsync("git", ["hash-object", "--", hashTarget], {
|
|
598
|
-
cwd: topLevel,
|
|
599
|
-
encoding: "utf8",
|
|
600
|
-
});
|
|
601
|
-
return headBlob === stdout.trim();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function inspectItemPath(value: InspectItem): string {
|
|
605
|
-
if (typeof value.source?.path === "string") return value.source.path;
|
|
606
|
-
if (typeof value.path === "string") return value.path;
|
|
607
|
-
return "";
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function isInspectItemActive(value: InspectItem): boolean {
|
|
611
|
-
return value.disabled !== true && value.enabled !== false && value.compatibilityStatus !== "disabled";
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/** Collect active projectInstruction paths for HEAD-blob provenance resolution. */
|
|
615
|
-
export function listActiveProjectInstructionPaths(document: Readonly<Record<string, unknown>>): readonly string[] {
|
|
616
|
-
const items = document.projectInstructions;
|
|
617
|
-
if (!Array.isArray(items)) return [];
|
|
618
|
-
const paths: string[] = [];
|
|
619
|
-
for (const value of items as InspectItem[]) {
|
|
620
|
-
if (!isInspectItemActive(value)) continue;
|
|
621
|
-
const path = inspectItemPath(value);
|
|
622
|
-
if (path !== "") paths.push(path);
|
|
623
|
-
}
|
|
624
|
-
return paths;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/** Classify first-party inspect JSON by provenance; wording and item counts are irrelevant. */
|
|
628
|
-
export function classifyGrokInspection(
|
|
629
|
-
document: Readonly<Record<string, unknown>>,
|
|
630
|
-
packageRoot: string,
|
|
631
|
-
options: GrokInspectionClassificationOptions = {},
|
|
632
|
-
): GrokControlledInspection {
|
|
633
|
-
const privateActive = new Set<string>();
|
|
634
|
-
const akActive = new Set<string>();
|
|
635
|
-
const headMatched = options.headMatchedProjectInstructionPaths ?? new Set<string>();
|
|
636
|
-
const externalCompat = document.externalCompat as { cells?: unknown } | undefined;
|
|
637
|
-
if (Array.isArray(externalCompat?.cells)) {
|
|
638
|
-
for (const cell of externalCompat.cells as Array<{ vendor?: unknown; surface?: unknown; enabled?: unknown }>) {
|
|
639
|
-
if (cell.enabled !== true) continue;
|
|
640
|
-
privateActive.add(`externalCompat:${String(cell.vendor)}:${String(cell.surface)}`);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
for (const section of ["skills", "agents", "plugins", "mcpServers", "hooks", "projectInstructions"] as const) {
|
|
644
|
-
const items = document[section];
|
|
645
|
-
if (!Array.isArray(items)) continue;
|
|
646
|
-
for (const value of items as InspectItem[]) {
|
|
647
|
-
if (!isInspectItemActive(value)) continue;
|
|
648
|
-
const sourceType = value.source?.type;
|
|
649
|
-
const path = inspectItemPath(value);
|
|
650
|
-
const identity = `${section}:${typeof value.name === "string" ? value.name : path}`;
|
|
651
|
-
if (sourceType === "builtin" || sourceType === "bundled") continue;
|
|
652
|
-
if (path === packageRoot || path.startsWith(`${packageRoot}/`)) akActive.add(identity);
|
|
653
|
-
else if (section === "projectInstructions" && headMatched.has(path)) continue;
|
|
654
|
-
else privateActive.add(identity);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return { privateActive: [...privateActive].sort(), akActive: [...akActive].sort() };
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/** AK Fixer PreToolUse seatbelt files written under controlled GROK_HOME/hooks. */
|
|
661
|
-
export const AK_BASH_SEATBELT_HOOK_FILES = ["ak-bash-seatbelt.json", "ak-bash-seatbelt.mjs"] as const;
|
|
662
|
-
|
|
663
|
-
function isMissingPathError(error: unknown): boolean {
|
|
664
|
-
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/** Refuse symlink roots so copy/rm never follow a redirected controlled home (#594 F4). */
|
|
668
|
-
export async function assertControlledGrokHomeIsRealDirectory(controlledHome: string): Promise<void> {
|
|
669
|
-
let st;
|
|
670
|
-
try {
|
|
671
|
-
st = await lstat(controlledHome);
|
|
672
|
-
} catch (error) {
|
|
673
|
-
if (isMissingPathError(error)) return;
|
|
674
|
-
throw error;
|
|
675
|
-
}
|
|
676
|
-
if (st.isSymbolicLink()) {
|
|
677
|
-
throw new Error(`controlled grok home must not be a symlink: ${controlledHome}`);
|
|
678
|
-
}
|
|
679
|
-
if (!st.isDirectory()) {
|
|
680
|
-
throw new Error(`controlled grok home must be a real directory: ${controlledHome}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/** Refuse a symlink credential destination before copy or scrub (#594 F4). */
|
|
685
|
-
export async function assertControlledGrokAuthIsNotSymlink(authPath: string): Promise<void> {
|
|
686
|
-
let st;
|
|
687
|
-
try {
|
|
688
|
-
st = await lstat(authPath);
|
|
689
|
-
} catch (error) {
|
|
690
|
-
if (isMissingPathError(error)) return;
|
|
691
|
-
throw error;
|
|
692
|
-
}
|
|
693
|
-
if (st.isSymbolicLink()) {
|
|
694
|
-
throw new Error(`controlled grok auth must not be a symlink: ${authPath}`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/** Remove AK seatbelt hook residue while leaving sessions/ intact (#594 F1). */
|
|
699
|
-
export async function scrubAkBashSeatbeltHooks(controlledHome: string): Promise<void> {
|
|
700
|
-
const hooksDir = join(controlledHome, "hooks");
|
|
701
|
-
for (const name of AK_BASH_SEATBELT_HOOK_FILES) {
|
|
702
|
-
await rm(join(hooksDir, name), { force: true });
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Copy only Grok's authentication authority into an otherwise isolated home.
|
|
708
|
-
* Refuses symlink home/auth destinations (no follow). Scrubs crash-window residual
|
|
709
|
-
* auth.json and AK seatbelt hooks before the copy so the next inspect cannot see
|
|
710
|
-
* either residue (#594 F1/F3/F4).
|
|
711
|
-
*/
|
|
712
|
-
export async function prepareControlledGrokHome(sourceHome: string, controlledHome: string): Promise<void> {
|
|
713
|
-
await assertControlledGrokHomeIsRealDirectory(controlledHome);
|
|
714
|
-
await mkdir(controlledHome, { recursive: true, mode: 0o700 });
|
|
715
|
-
await assertControlledGrokHomeIsRealDirectory(controlledHome);
|
|
716
|
-
const authPath = join(controlledHome, "auth.json");
|
|
717
|
-
await assertControlledGrokAuthIsNotSymlink(authPath);
|
|
718
|
-
// Crash-window residue: prior auth.json may still sit in the retained ledger.
|
|
719
|
-
await rm(authPath, { force: true });
|
|
720
|
-
await scrubAkBashSeatbeltHooks(controlledHome);
|
|
721
|
-
await copyFile(join(sourceHome, ".grok", "auth.json"), authPath);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/** First-party structured inspection under the exact environment used by ACP. */
|
|
725
|
-
export async function inspectControlledGrok(options: {
|
|
726
|
-
readonly binary: string;
|
|
727
|
-
readonly cwd: string;
|
|
728
|
-
readonly env: NodeJS.ProcessEnv;
|
|
729
|
-
readonly packageRoot: string;
|
|
730
|
-
}): Promise<GrokControlledInspection> {
|
|
731
|
-
const { stdout } = await execFileAsync(options.binary, ["inspect", "--json"], {
|
|
732
|
-
cwd: options.cwd,
|
|
733
|
-
env: options.env,
|
|
734
|
-
encoding: "utf8",
|
|
735
|
-
});
|
|
736
|
-
const document: unknown = JSON.parse(stdout);
|
|
737
|
-
if (typeof document !== "object" || document === null || Array.isArray(document)) {
|
|
738
|
-
throw new Error("Grok structured inspection did not return an object");
|
|
739
|
-
}
|
|
740
|
-
const record = document as Readonly<Record<string, unknown>>;
|
|
741
|
-
const headMatchedProjectInstructionPaths = new Set<string>();
|
|
742
|
-
for (const path of listActiveProjectInstructionPaths(record)) {
|
|
743
|
-
if (path === options.packageRoot || path.startsWith(`${options.packageRoot}/`)) continue;
|
|
744
|
-
if (await isHeadMatchedProjectInstruction(options.cwd, path)) {
|
|
745
|
-
headMatchedProjectInstructionPaths.add(path);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
return classifyGrokInspection(record, options.packageRoot, { headMatchedProjectInstructionPaths });
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/** Exact child environment shared by inspect and ACP agent processes. */
|
|
752
|
-
export function controlledGrokChildEnv(base: NodeJS.ProcessEnv, grokHome: string): NodeJS.ProcessEnv {
|
|
399
|
+
/** Child environment shared by ACP agent processes. */
|
|
400
|
+
export function controlledGrokChildEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
753
401
|
return {
|
|
754
402
|
...base,
|
|
755
403
|
...PRIVATE_COMPAT_ENV,
|
|
756
|
-
HOME: grokHome,
|
|
757
|
-
GROK_HOME: grokHome,
|
|
758
404
|
GROK_MEMORY: "0",
|
|
759
405
|
GROK_SUBAGENTS: "0",
|
|
760
406
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Closed host discriminators only; unknown previous/live hosts never inject.
|
|
4
4
|
*/
|
|
5
5
|
import { access, readdir } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
7
|
|
|
8
8
|
import type { RoleTurnHostTransition } from "./host-contracts.ts";
|
|
9
9
|
|
|
@@ -29,42 +29,33 @@ async function listPiNativeRecordPaths(sessionFile: string): Promise<string[]> {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Present sitian records.jsonl paths under sessionParent topology (#717).
|
|
34
|
+
* resolveSitianRecordPathInLedger writes dirname(sessionParent)/<category>/records.jsonl
|
|
35
|
+
* when sessionParent is inside ledger home — never session.jsonl itself.
|
|
36
|
+
*/
|
|
37
|
+
async function listSitianRecordPaths(sessionParent: string): Promise<string[]> {
|
|
38
|
+
const sessionRoot = dirname(sessionParent);
|
|
39
|
+
let entries;
|
|
36
40
|
try {
|
|
37
|
-
|
|
41
|
+
entries = await readdir(sessionRoot, { withFileTypes: true });
|
|
38
42
|
} catch (error) {
|
|
39
43
|
if (isEnoent(error)) return [];
|
|
40
44
|
throw error;
|
|
41
45
|
}
|
|
42
|
-
const
|
|
43
|
-
for (const
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
sessionDirs = await readdir(join(grokSessionsDir, cwdEntry.name), { withFileTypes: true });
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if (isEnoent(error)) continue;
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
for (const sessEntry of sessionDirs) {
|
|
53
|
-
if (!sessEntry.isDirectory()) continue;
|
|
54
|
-
updatesPaths.push(join(grokSessionsDir, cwdEntry.name, sessEntry.name, "updates.jsonl"));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
updatesPaths.sort();
|
|
58
|
-
const present: string[] = [];
|
|
59
|
-
for (const updatesFile of updatesPaths) {
|
|
46
|
+
const recordPaths: string[] = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!entry.isDirectory()) continue;
|
|
49
|
+
const recordFile = join(sessionRoot, entry.name, "records.jsonl");
|
|
60
50
|
try {
|
|
61
|
-
await access(
|
|
62
|
-
|
|
51
|
+
await access(recordFile);
|
|
52
|
+
recordPaths.push(recordFile);
|
|
63
53
|
} catch (error) {
|
|
64
54
|
if (!isEnoent(error)) throw error;
|
|
65
55
|
}
|
|
66
56
|
}
|
|
67
|
-
|
|
57
|
+
recordPaths.sort();
|
|
58
|
+
return recordPaths;
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
/**
|
|
@@ -72,13 +63,13 @@ export async function listGrokNativeRecordPaths(runDirectory: string): Promise<s
|
|
|
72
63
|
* Unknown host names → undefined (no inject). Empty native volume still
|
|
73
64
|
* yields a typed switch (empty path list).
|
|
74
65
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
66
|
+
* Pi previous → Pi session.jsonl path.
|
|
67
|
+
* Grok previous → sitian records on the live run (#717). Grok CLI journals
|
|
68
|
+
* stay in the operator grok home; they are not copied here.
|
|
77
69
|
*/
|
|
78
70
|
export async function projectHostTransitionPriorNative(input: {
|
|
79
71
|
readonly previousHost: string;
|
|
80
72
|
readonly liveHost: string;
|
|
81
|
-
readonly runDirectory: string;
|
|
82
73
|
readonly piSessionFile: string;
|
|
83
74
|
}): Promise<RoleTurnHostTransition | undefined> {
|
|
84
75
|
if (input.previousHost === input.liveHost) return undefined;
|
|
@@ -92,8 +83,8 @@ export async function projectHostTransitionPriorNative(input: {
|
|
|
92
83
|
priorNativePaths: paths,
|
|
93
84
|
};
|
|
94
85
|
}
|
|
95
|
-
// previousHost === "grok-build":
|
|
96
|
-
const paths = await
|
|
86
|
+
// previousHost === "grok-build": sitian path handoff only — do not read bytes.
|
|
87
|
+
const paths = await listSitianRecordPaths(input.piSessionFile);
|
|
97
88
|
return {
|
|
98
89
|
previousHost: "grok-build",
|
|
99
90
|
priorNativePaths: paths,
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -294,6 +294,11 @@ export type CliEnv = {
|
|
|
294
294
|
/** Override Notary role-run timeout (tests). */
|
|
295
295
|
notaryTimeoutMs?: number;
|
|
296
296
|
createRunId?: () => string;
|
|
297
|
+
/**
|
|
298
|
+
* #724: set by the `new` support verb before role dispatch; seat runners
|
|
299
|
+
* skip same-ticket auto-resume. Not a public flag — the verb is the choice.
|
|
300
|
+
*/
|
|
301
|
+
freshSummons?: true;
|
|
297
302
|
};
|
|
298
303
|
|
|
299
304
|
/** Compose the Pi turn host for one role dispatch (sole public-cli → pi contact). */
|
|
@@ -437,6 +442,7 @@ function createRoleEnvironment(
|
|
|
437
442
|
...(options.config?.autoResumeLimit === undefined
|
|
438
443
|
? {}
|
|
439
444
|
: { autoResumeLimit: options.config.autoResumeLimit }),
|
|
445
|
+
...(env.freshSummons === true ? { freshSummons: true as const } : {}),
|
|
440
446
|
};
|
|
441
447
|
}
|
|
442
448
|
|
|
@@ -1084,12 +1090,14 @@ export async function runAkRole(
|
|
|
1084
1090
|
packageRoot: await realpath(env.packageRoot),
|
|
1085
1091
|
principalAuthority: env.principalAuthority ?? piDurablePrincipalAuthority,
|
|
1086
1092
|
};
|
|
1087
|
-
|
|
1088
|
-
// Host/engine axes: callable roles + resume (#617 DK-3
|
|
1093
|
+
let parsed = parseArgv(argv);
|
|
1094
|
+
// Host/engine axes: callable roles + resume + new (#617 DK-3; #724 new shares seat axes).
|
|
1089
1095
|
// Support commands (roles/config/…) still refuse both flags.
|
|
1090
1096
|
const acceptsSeatAxes =
|
|
1091
1097
|
parsed.command !== undefined &&
|
|
1092
|
-
(isPublicCallableRole(parsed.command) ||
|
|
1098
|
+
(isPublicCallableRole(parsed.command) ||
|
|
1099
|
+
parsed.command === "resume" ||
|
|
1100
|
+
parsed.command === "new");
|
|
1093
1101
|
if (
|
|
1094
1102
|
parsed.host !== undefined &&
|
|
1095
1103
|
!parsed.help &&
|
|
@@ -1160,6 +1168,24 @@ export async function runAkRole(
|
|
|
1160
1168
|
};
|
|
1161
1169
|
}
|
|
1162
1170
|
|
|
1171
|
+
// #724 explicit fresh summons: `ak-role new <role> …` — same role argv, always mint.
|
|
1172
|
+
// Rewrites onto the role command with freshSummons; auto-resume and resume stay intact.
|
|
1173
|
+
if (parsed.command === "new") {
|
|
1174
|
+
const role = parsed.args[0];
|
|
1175
|
+
if (role === undefined) {
|
|
1176
|
+
throw new CliUsageError("usage: ak-role new <role> …");
|
|
1177
|
+
}
|
|
1178
|
+
if (!isPublicCallableRole(role)) {
|
|
1179
|
+
throw new CliUsageError(`usage: ak-role new <role> …; unknown role: ${role}`);
|
|
1180
|
+
}
|
|
1181
|
+
parsed = {
|
|
1182
|
+
...parsed,
|
|
1183
|
+
command: role,
|
|
1184
|
+
args: parsed.args.slice(1),
|
|
1185
|
+
};
|
|
1186
|
+
env = { ...env, freshSummons: true };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1163
1189
|
// Resume reopens an exact Role run (#416): caller decides; session principal
|
|
1164
1190
|
// must still exist. Seat and dispatch follow the durable admitted role.
|
|
1165
1191
|
// #471: unique parser owns {runId, message?}; five role paths only consume it.
|
|
@@ -1202,7 +1228,10 @@ export async function runAkRole(
|
|
|
1202
1228
|
return cliResultFromRoleRun(result);
|
|
1203
1229
|
}
|
|
1204
1230
|
|
|
1205
|
-
if (
|
|
1231
|
+
if (
|
|
1232
|
+
parsed.command !== undefined &&
|
|
1233
|
+
isPublicCliSupportCommand(parsed.command)
|
|
1234
|
+
) {
|
|
1206
1235
|
throw new CliUsageError(`unhandled support command: ${parsed.command}`);
|
|
1207
1236
|
}
|
|
1208
1237
|
|
|
@@ -1271,6 +1271,17 @@ const SUPPORT_COMMAND_HELP = {
|
|
|
1271
1271
|
"ak-role --engine agy resume 01abc…",
|
|
1272
1272
|
],
|
|
1273
1273
|
},
|
|
1274
|
+
new: {
|
|
1275
|
+
command: "new",
|
|
1276
|
+
summary:
|
|
1277
|
+
"Explicit fresh summons: same role options as a direct call, but always mint a new run (skip same-ticket auto-resume). Caller chooses new vs resume; no overflow judgment.",
|
|
1278
|
+
usage: ["ak-role new <role> [options] …"],
|
|
1279
|
+
examples: [
|
|
1280
|
+
'ak-role new countersign --attach ./ticket.md "裁:本票 #708 是否足以开工。"',
|
|
1281
|
+
"ak-role new notary --source-run 01abc…@countersign",
|
|
1282
|
+
"ak-role --host grok-build new inspector --attach ./change.patch",
|
|
1283
|
+
],
|
|
1284
|
+
},
|
|
1274
1285
|
} as const satisfies Record<string, PublicCommandHelpFacts>;
|
|
1275
1286
|
|
|
1276
1287
|
/**
|
|
@@ -110,6 +110,11 @@ export type PostAdmissionEnv = {
|
|
|
110
110
|
sessionAppender: SessionCustomEntryAppender;
|
|
111
111
|
autoResumeLimit?: number;
|
|
112
112
|
createRunId?: () => string;
|
|
113
|
+
/**
|
|
114
|
+
* #724 explicit fresh summons (`ak-role new <role>`): skip same-ticket auto-resume
|
|
115
|
+
* and mint a new run. Absent on ordinary role commands and on `ak-role resume`.
|
|
116
|
+
*/
|
|
117
|
+
freshSummons?: true;
|
|
113
118
|
};
|
|
114
119
|
|
|
115
120
|
/**
|
|
@@ -294,7 +299,6 @@ export async function dispatchPostAdmissionTurn<
|
|
|
294
299
|
? await projectHostTransitionPriorNative({
|
|
295
300
|
previousHost,
|
|
296
301
|
liveHost,
|
|
297
|
-
runDirectory: admitted.runDirectory,
|
|
298
302
|
piSessionFile: principalCoordinates.sessionFile,
|
|
299
303
|
})
|
|
300
304
|
: undefined;
|