@akagilnc/pi-workflow-roles 0.1.3545 → 0.1.3549

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.
@@ -1,12 +1,8 @@
1
- import { execFile, spawn } from "node:child_process";
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), not inspect.akActive.
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
- type InspectItem = {
440
- readonly name?: unknown;
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
- /** Native Grok updates.jsonl paths under runDirectory/grok-home/sessions, sorted. */
33
- export async function listGrokNativeRecordPaths(runDirectory: string): Promise<string[]> {
34
- const grokSessionsDir = join(runDirectory, "grok-home", "sessions");
35
- let encodedCwds;
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
- encodedCwds = await readdir(grokSessionsDir, { withFileTypes: true });
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 updatesPaths: string[] = [];
43
- for (const cwdEntry of encodedCwds) {
44
- if (!cwdEntry.isDirectory()) continue;
45
- let sessionDirs;
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(updatesFile);
62
- present.push(updatesFile);
51
+ await access(recordFile);
52
+ recordPaths.push(recordFile);
63
53
  } catch (error) {
64
54
  if (!isEnoent(error)) throw error;
65
55
  }
66
56
  }
67
- return present;
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
- * Grok native home lives under the live run that wrote it (#617 DK-4 / #637
76
- * same-run resume). No cross-run previousRunDirectory override.
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": DK-7 path handoff only — do not read bytes.
96
- const paths = await listGrokNativeRecordPaths(input.runDirectory);
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,
@@ -294,7 +294,6 @@ export async function dispatchPostAdmissionTurn<
294
294
  ? await projectHostTransitionPriorNative({
295
295
  previousHost,
296
296
  liveHost,
297
- runDirectory: admitted.runDirectory,
298
297
  piSessionFile: principalCoordinates.sessionFile,
299
298
  })
300
299
  : undefined;
@@ -1,69 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- import {
5
- FIXER_BASH_FORBIDDEN_LITERALS,
6
- fixerBashSeatbeltDenyReason,
7
- } from "../fixer-bash-seatbelt.ts";
8
-
9
- /**
10
- * Render the Grok PreToolUse hook script.
11
- * Literals and deny reason come from the single ADR 0008 authority; this file is
12
- * transport-only (stdin event → stdout decision JSON for the Grok hook runner).
13
- */
14
- export function renderGrokBashSeatbeltHookScript(): string {
15
- // Reason templates are baked from the authority so the standalone hook process
16
- // cannot drift from fixerBashSeatbeltDenyReason without editing this render call.
17
- const reasons = Object.fromEntries(
18
- FIXER_BASH_FORBIDDEN_LITERALS.map((literal) => [literal, fixerBashSeatbeltDenyReason(literal)]),
19
- );
20
- return `#!/usr/bin/env node
21
- const LITERALS = ${JSON.stringify([...FIXER_BASH_FORBIDDEN_LITERALS])};
22
- const REASONS = ${JSON.stringify(reasons)};
23
- let raw = "";
24
- process.stdin.setEncoding("utf8");
25
- process.stdin.on("data", (chunk) => { raw += chunk; });
26
- process.stdin.on("end", () => {
27
- let event = {};
28
- try { event = JSON.parse(raw); } catch { /* fail-open on malformed host payload */ }
29
- const input = event && typeof event === "object" ? event.toolInput : undefined;
30
- const command = input && typeof input === "object" && typeof input.command === "string"
31
- ? input.command : undefined;
32
- const matched = typeof command === "string"
33
- ? LITERALS.find((literal) => command.includes(literal))
34
- : undefined;
35
- const decision = matched === undefined
36
- ? { decision: "allow" }
37
- : { decision: "deny", reason: REASONS[matched] };
38
- process.stdout.write(JSON.stringify(decision));
39
- });
40
- `;
41
- }
42
-
43
- /**
44
- * Install the AK bash seatbelt into a controlled Grok home (GROK_HOME root).
45
- * Never writes the operator's private ~/.grok — callers must pass the isolated home.
46
- */
47
- export async function installGrokPreToolUseDeny(controlledHome: string): Promise<void> {
48
- const hooksDir = join(controlledHome, "hooks");
49
- await mkdir(hooksDir, { recursive: true });
50
- const scriptName = "ak-bash-seatbelt.mjs";
51
- const scriptPath = join(hooksDir, scriptName);
52
- await writeFile(scriptPath, renderGrokBashSeatbeltHookScript(), { mode: 0o755 });
53
- await writeFile(join(hooksDir, "ak-bash-seatbelt.json"), `${JSON.stringify({
54
- hooks: {
55
- PreToolUse: [
56
- {
57
- matcher: "Bash|run_terminal_command",
58
- hooks: [
59
- {
60
- type: "command",
61
- command: process.execPath + " " + JSON.stringify(scriptPath),
62
- timeout: 5,
63
- },
64
- ],
65
- },
66
- ],
67
- },
68
- }, null, 2)}\n`);
69
- }