@themoltnet/pi-runtime 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -49,10 +49,24 @@ export declare interface AllowedToolsClient {
49
49
  argvPrefix: string[];
50
50
  }>;
51
51
  runtimeKind: string;
52
+ policySnapshotHash?: string;
53
+ runtimeProfileRevision?: number;
52
54
  }>;
53
55
  };
54
56
  }
55
57
 
58
+ export declare function assertGuestEnvironmentBoundary(options: {
59
+ guestCredentialMode: GuestCredentialMode;
60
+ forwardEnv?: readonly string[];
61
+ sandboxEnv?: Readonly<Record<string, string>>;
62
+ }): void;
63
+
64
+ /** @deprecated Prefer assertGuestEnvironmentBoundary for mode-aware checks. */
65
+ export declare function assertHostAuthenticatedGuestEnvironment(options: {
66
+ forwardEnv?: readonly string[];
67
+ sandboxEnv?: Readonly<Record<string, string>>;
68
+ }): void;
69
+
56
70
  /**
57
71
  * Construct an `AgentSession`. By default it is in-memory; callers may opt
58
72
  * parent sessions into daemon-owned file persistence via `sessionPersistence`.
@@ -390,6 +404,10 @@ export declare function executePiTask(claimedTask: ClaimedTask, reporter: TaskRe
390
404
  export declare interface ExecutePiTaskOptions {
391
405
  /** MoltNet agent whose credentials the VM boots with. */
392
406
  agentName: string;
407
+ /** Already-authenticated host-side Agent. Daemon callers always supply it. */
408
+ moltnetAgent?: Agent;
409
+ /** Explicit trust boundary for MoltNet credentials inside the guest. */
410
+ guestCredentialMode?: GuestCredentialMode;
393
411
  /**
394
412
  * Host root that owns `.moltnet/<agentName>/`.
395
413
  *
@@ -430,8 +448,8 @@ export declare interface ExecutePiTaskOptions {
430
448
  runtimeProfileContext?: readonly ContextRef[];
431
449
  /**
432
450
  * Runtime profile id, used to resolve the tool-policy allow-set at session
433
- * start. Required together with a non-`off` `toolEnforcement` for the
434
- * `tool_call` gate to run.
451
+ * start. Required with `toolEnforcement`; the resolved mode determines
452
+ * whether a `tool_call` gate is installed.
435
453
  */
436
454
  runtimeProfileId?: string;
437
455
  /** Tool-policy enforcement mode for the selected runtime profile. */
@@ -447,6 +465,8 @@ export declare interface ExecutePiTaskOptions {
447
465
  promptExtras?: Record<string, unknown>;
448
466
  /** Snapshot progress callback; defaults to stderr logging. */
449
467
  onSnapshotProgress?: (message: string) => void;
468
+ /** Structured VM credential-boundary diagnostics. */
469
+ onVmDiagnostic?: (diagnostic: VmDiagnostic) => void;
450
470
  /**
451
471
  * Optional pre-resolved checkpoint path. If omitted, `ensureSnapshot` is
452
472
  * invoked. Useful for batch execution where the caller wants to cache
@@ -586,21 +606,18 @@ export declare function filterModelVisibleTools(tools: readonly ToolDefinition[]
586
606
  */
587
607
  export declare function findMainWorktree(startPath?: string): string;
588
608
 
589
- /**
590
- * The gate's verdict:
591
- * - `{ allow: true }` — let the tool run.
592
- * - `{ allow: false, reason }` — block it (enforce mode).
593
- * - `{ audit, ... }` — would-block, but proceed and record it (watch mode).
594
- */
595
609
  export declare type GateDecision = {
596
610
  allow: true;
611
+ reasonCode: ToolPolicyDecisionReason;
597
612
  matchedShellCommands?: MatchedShellCommand[];
598
613
  } | {
599
614
  allow: false;
615
+ reasonCode: ToolPolicyDecisionReason;
600
616
  reason: string;
601
617
  missing?: string[];
602
618
  missingShellCommands?: MissingShellCommand[];
603
619
  } | {
620
+ reasonCode: ToolPolicyDecisionReason;
604
621
  audit: string;
605
622
  missing?: string[];
606
623
  missingShellCommands?: MissingShellCommand[];
@@ -642,6 +659,13 @@ export declare interface GondolinTemplateResolveContext {
642
659
  onProgress?: (message: string) => void;
643
660
  }
644
661
 
662
+ export declare type GuestCredentialMode = 'guest-config' | 'host-authenticated';
663
+
664
+ export declare class GuestEnvironmentBoundaryError extends Error {
665
+ readonly refusedNames: readonly string[];
666
+ constructor(refusedNames: readonly string[]);
667
+ }
668
+
645
669
  /**
646
670
  * Baseline env keys forwarded to host-exec child processes.
647
671
  * Callers can extend this set at sandbox startup via `MoltNetToolsConfig.hostExecBaseEnv`.
@@ -703,7 +727,7 @@ export declare function isResolvedPathInsideRoot(path: string, root: string): bo
703
727
 
704
728
  export declare function isToolVisible(name: string, policy?: ModelVisibleToolPolicy): boolean;
705
729
 
706
- export declare function loadCredentials(agentDir: string): VmCredentials;
730
+ export declare function loadCredentials(agentDir: string, mode?: GuestCredentialMode, onDiagnostic?: (diagnostic: VmDiagnostic) => void): VmCredentials;
707
731
 
708
732
  export declare interface ManagedVm {
709
733
  vm: VM;
@@ -1076,11 +1100,14 @@ export declare interface ResolvedGondolinTemplate {
1076
1100
  resumeCommands: readonly ResumeCommand[];
1077
1101
  }
1078
1102
 
1103
+ export declare function resolveHostExecBaseEnv(guestCredentialMode: GuestCredentialMode, agentEnv: Readonly<Record<string, string | undefined>>): Set<string>;
1104
+
1079
1105
  /**
1080
1106
  * Resolve the session's tool policy at start-up.
1081
1107
  *
1082
- * `off` short-circuits without a network call. Otherwise the allowed-tool set
1083
- * is fetched from the API. If that fetch fails, the mode decides the fallback:
1108
+ * The latest allowed-tool set and enforcement mode are fetched from the API,
1109
+ * including when the daemon's cached profile says `off`. If that fetch fails,
1110
+ * the cached mode decides the fallback:
1084
1111
  * `enforce` **fails closed** (empty allow-set → every non-`off` tool is
1085
1112
  * blocked); `watch` fails open (empty allow-set → every tool is audited but
1086
1113
  * allowed).
@@ -1264,6 +1291,10 @@ export declare interface SessionToolPolicy {
1264
1291
  enforcement: ToolEnforcement;
1265
1292
  allowedTools: ReadonlySet<string>;
1266
1293
  allowedShellCommands: readonly ShellCommandRule[];
1294
+ /** Latest effective policy hash resolved for this Pi session. */
1295
+ executionPolicySnapshotHash?: string;
1296
+ /** Latest runtime profile revision resolved with the session policy. */
1297
+ executionRuntimeProfileRevision?: number;
1267
1298
  /**
1268
1299
  * `true` when the allow-set is a **degraded fallback** — the allowed-tools
1269
1300
  * fetch failed or timed out and this policy is the fail-closed/fail-open
@@ -1271,7 +1302,7 @@ export declare interface SessionToolPolicy {
1271
1302
  * empty-but-resolved policy (e.g. a profile with no bound tools) has
1272
1303
  * `degraded: false`. Surfaced in every audit/block log so an operator can tell
1273
1304
  * "blocked because the policy is empty" from "blocked because we couldn't read
1274
- * the policy". `off` and successful resolutions are never degraded.
1305
+ * the policy". Successful resolutions are never degraded.
1275
1306
  */
1276
1307
  degraded: boolean;
1277
1308
  }
@@ -1327,10 +1358,35 @@ declare const ToolEnforcementSchema = Type.Union(toolEnforcementLiterals, {
1327
1358
  'Runtime tool-policy enforcement mode: off (inert), watch (audit only), enforce (block disallowed tools, fail-closed).',
1328
1359
  });
1329
1360
 
1361
+ /** Correlation and claim evidence repeated on every tool-policy decision. */
1362
+ export declare interface ToolPolicyDecisionContext {
1363
+ taskId: string;
1364
+ attemptN: number;
1365
+ teamId: string;
1366
+ claimantAgentId?: string;
1367
+ leaseId?: string;
1368
+ proposerKind?: 'agent' | 'human';
1369
+ proposerId?: string;
1370
+ claimRuntimeProfileId?: string;
1371
+ executionRuntimeProfileId?: string;
1372
+ claimRuntimeProfileRevision?: number;
1373
+ claimPolicySnapshotHash?: string;
1374
+ claimedExecutorFingerprint?: string;
1375
+ }
1376
+
1377
+ /**
1378
+ * The gate's verdict:
1379
+ * - `{ allow: true }` — let the tool run.
1380
+ * - `{ allow: false, reason }` — block it (enforce mode).
1381
+ * - `{ audit, ... }` — would-block, but proceed and record it (watch mode).
1382
+ */
1383
+ export declare type ToolPolicyDecisionReason = 'policy_off' | 'executor_protocol_tool' | 'policy_allowed' | 'shell_command_prefix_allowed' | 'shell_command_unresolvable' | 'arbitrary_code_interpreter' | 'shell_output_redirection_requires_broad_permission' | 'tool_not_permitted';
1384
+
1330
1385
  export declare interface ToolPolicyExtensionDeps {
1331
1386
  policy: SessionToolPolicy;
1332
1387
  analyzer: ShellCommandAnalyzer;
1333
1388
  logger: ToolPolicyLogger;
1389
+ context?: ToolPolicyDecisionContext;
1334
1390
  }
1335
1391
 
1336
1392
  /**
@@ -1366,6 +1422,12 @@ export declare interface VmConfig {
1366
1422
  checkpointPath: string;
1367
1423
  /** MoltNet agent name (used to resolve credentials). */
1368
1424
  agentName: string;
1425
+ /**
1426
+ * Trust boundary for guest credentials. `guest-config` injects the complete
1427
+ * legacy agent directory. `host-authenticated` never reads or injects it and
1428
+ * relies exclusively on the supplied host-side Agent for MoltNet operations.
1429
+ */
1430
+ guestCredentialMode?: GuestCredentialMode;
1369
1431
  /**
1370
1432
  * Host root that owns `.moltnet/<agentName>/`.
1371
1433
  *
@@ -1389,12 +1451,16 @@ export declare interface VmConfig {
1389
1451
  * into the guest without storing secret values in the profile.
1390
1452
  */
1391
1453
  forwardEnv?: string[];
1454
+ /** Structured credential-boundary diagnostics for daemon loggers. */
1455
+ onDiagnostic?: (diagnostic: VmDiagnostic) => void;
1392
1456
  /** Abort resume/setup work, closing any live VM owned by resumeVm. */
1393
1457
  signal?: AbortSignal;
1394
1458
  }
1395
1459
 
1396
1460
  export declare interface VmCredentials {
1461
+ /** Empty in host-authenticated mode; retained as strings for API stability. */
1397
1462
  moltnetJson: string;
1463
+ /** Empty in host-authenticated mode; retained as strings for API stability. */
1398
1464
  agentEnvRaw: string;
1399
1465
  /**
1400
1466
  * Pi OAuth/API-key auth blob. Null when neither `~/.pi/agent/auth.json`
@@ -1415,6 +1481,13 @@ export declare interface VmCredentials {
1415
1481
  githubAppPemFilename: string | null;
1416
1482
  }
1417
1483
 
1484
+ export declare interface VmDiagnostic {
1485
+ event: 'vm.credentials.mode' | 'vm.credentials.github_key_missing';
1486
+ level: 'info' | 'warning';
1487
+ message: string;
1488
+ credentialMode: GuestCredentialMode;
1489
+ }
1490
+
1418
1491
  /**
1419
1492
  * Subset of `@earendil-works/gondolin`'s `VmFs` we actually use. We
1420
1493
  * narrow the dependency surface so unit tests can hand in a
package/dist/index.js CHANGED
@@ -1925,6 +1925,11 @@ Type$1.Object({ id: UuidSchema });
1925
1925
  Type$1.Object({
1926
1926
  publicKey: PublicKeySchema,
1927
1927
  fingerprint: FingerprintSchema,
1928
+ proof: Type$1.String({
1929
+ minLength: 1,
1930
+ maxLength: 256
1931
+ }),
1932
+ credentialType: Type$1.Literal("oauth2"),
1928
1933
  agentName: Type$1.String({
1929
1934
  minLength: 1,
1930
1935
  maxLength: 34
@@ -2198,7 +2203,6 @@ var ProblemCodeSchema = Type$1.Union([
2198
2203
  Type$1.Literal("VALIDATION_FAILED"),
2199
2204
  Type$1.Literal("INVALID_CHALLENGE"),
2200
2205
  Type$1.Literal("INVALID_SIGNATURE"),
2201
- Type$1.Literal("VOUCHER_LIMIT"),
2202
2206
  Type$1.Literal("RATE_LIMIT_EXCEEDED"),
2203
2207
  Type$1.Literal("SERIALIZATION_EXHAUSTED"),
2204
2208
  Type$1.Literal("SIGNING_REQUEST_EXPIRED"),
@@ -4532,12 +4536,30 @@ function resolveVmAgentDir(config) {
4532
4536
  const rootDir = config.agentRootDir ?? findMainWorktree();
4533
4537
  return path.join(rootDir, ".moltnet", config.agentName);
4534
4538
  }
4535
- function loadCredentials(agentDir) {
4536
- const moltnetJson = readFileSync(path.join(agentDir, "moltnet.json"), "utf8");
4537
- const agentEnvRaw = readFileSync(path.join(agentDir, "env"), "utf8");
4539
+ function loadCredentials(agentDir, mode = "guest-config", onDiagnostic) {
4540
+ const moltnetPath = path.join(agentDir, "moltnet.json");
4541
+ const agentEnvPath = path.join(agentDir, "env");
4538
4542
  const piAgentDir = resolvePiCodingAgentDir();
4539
4543
  const piAuthPath = path.join(piAgentDir, "auth.json");
4540
4544
  const piAuthJson = existsSync(piAuthPath) ? readFileSync(piAuthPath, "utf8") : null;
4545
+ if (mode === "host-authenticated") return {
4546
+ moltnetJson: "",
4547
+ agentEnvRaw: "",
4548
+ piAuthJson,
4549
+ agentEnv: {},
4550
+ gitconfig: null,
4551
+ sshPrivateKey: null,
4552
+ sshPublicKey: null,
4553
+ allowedSigners: null,
4554
+ githubAppPem: null,
4555
+ githubAppPemFilename: null
4556
+ };
4557
+ const hasMoltnetJson = existsSync(moltnetPath);
4558
+ const hasAgentEnv = existsSync(agentEnvPath);
4559
+ if (!hasMoltnetJson || !hasAgentEnv) throw new Error(`Guest credential mode requires both ${moltnetPath} and ${agentEnvPath}`);
4560
+ const moltnetJson = readFileSync(moltnetPath, "utf8");
4561
+ const agentEnvRaw = readFileSync(agentEnvPath, "utf8");
4562
+ if (moltnetJson.trim() === "") throw new Error(`Agent configuration is empty: ${moltnetPath}`);
4541
4563
  const gitconfigPath = path.join(agentDir, "gitconfig");
4542
4564
  const gitconfig = existsSync(gitconfigPath) ? readFileSync(gitconfigPath, "utf8") : null;
4543
4565
  const sshDir = path.join(agentDir, "ssh");
@@ -4546,9 +4568,13 @@ function loadCredentials(agentDir) {
4546
4568
  const allowedSigners = existsSync(path.join(sshDir, "allowed_signers")) ? readFileSync(path.join(sshDir, "allowed_signers"), "utf8") : null;
4547
4569
  let githubAppPem = null;
4548
4570
  let githubAppPemFilename = null;
4549
- const pemPath = JSON.parse(moltnetJson).github?.private_key_path;
4550
- if (pemPath) if (!existsSync(pemPath)) process.stderr.write(`[pi-extension] Warning: github.private_key_path not found at ${pemPath} — moltnet github token will fail inside the guest
4551
- `);
4571
+ const pemPath = (moltnetJson ? JSON.parse(moltnetJson) : null)?.github?.private_key_path;
4572
+ if (pemPath) if (!existsSync(pemPath)) onDiagnostic?.({
4573
+ event: "vm.credentials.github_key_missing",
4574
+ level: "warning",
4575
+ credentialMode: mode,
4576
+ message: `github.private_key_path not found at ${pemPath}; moltnet github token will fail inside the guest`
4577
+ });
4552
4578
  else {
4553
4579
  githubAppPem = readFileSync(pemPath, "utf8");
4554
4580
  githubAppPemFilename = path.basename(pemPath);
@@ -4594,6 +4620,70 @@ var BASE_ALLOWED_HOSTS = [
4594
4620
  "storage.googleapis.com",
4595
4621
  "*.googlesource.com"
4596
4622
  ];
4623
+ var DEFAULT_MOLTNET_API_URL = "https://api.themolt.net";
4624
+ /**
4625
+ * Host environment names that may intentionally cross into a
4626
+ * host-authenticated guest. This local list is the authority boundary;
4627
+ * server-supplied runtime-profile `requiredEnv` cannot widen it.
4628
+ */
4629
+ var HOST_AUTHENTICATED_GUEST_ENV_ALLOWLIST = new Set([
4630
+ "ANTHROPIC_API_KEY",
4631
+ "OPENAI_API_KEY",
4632
+ "OPENAI_BASE_URL",
4633
+ "AZURE_OPENAI_API_KEY",
4634
+ "AZURE_OPENAI_ENDPOINT",
4635
+ "AZURE_OPENAI_API_VERSION",
4636
+ "GOOGLE_API_KEY",
4637
+ "GEMINI_API_KEY",
4638
+ "MISTRAL_API_KEY",
4639
+ "GROQ_API_KEY",
4640
+ "OPENROUTER_API_KEY",
4641
+ "XAI_API_KEY",
4642
+ "CEREBRAS_API_KEY",
4643
+ "DEEPSEEK_API_KEY",
4644
+ "OLLAMA_API_KEY",
4645
+ "OLLAMA_BASE_URL",
4646
+ "AWS_ACCESS_KEY_ID",
4647
+ "AWS_SECRET_ACCESS_KEY",
4648
+ "AWS_SESSION_TOKEN",
4649
+ "AWS_REGION",
4650
+ "AWS_DEFAULT_REGION",
4651
+ "GITHUB_TOKEN",
4652
+ "GH_TOKEN",
4653
+ "LINEAR_API_KEY"
4654
+ ]);
4655
+ var RESERVED_GUEST_ENVIRONMENT_NAMES = new Set([
4656
+ "PATH",
4657
+ "HOME",
4658
+ "NODE_EXTRA_CA_CERTS",
4659
+ "MOLTNET_GUEST_WORKSPACE",
4660
+ "GIT_SSH",
4661
+ "GIT_SSH_COMMAND",
4662
+ "SSH_AUTH_SOCK"
4663
+ ]);
4664
+ function isReservedGuestEnvironmentName(name) {
4665
+ return name.startsWith("MOLTNET_") || name.startsWith("GIT_CONFIG_") || RESERVED_GUEST_ENVIRONMENT_NAMES.has(name);
4666
+ }
4667
+ var GuestEnvironmentBoundaryError = class extends Error {
4668
+ constructor(refusedNames) {
4669
+ super(`Guest credential boundary refuses runtime-controlled environment variables: ${refusedNames.join(", ")}. Remove them from the runtime profile; MoltNet operations use the trusted host-side Agent.`);
4670
+ this.refusedNames = refusedNames;
4671
+ this.name = "GuestEnvironmentBoundaryError";
4672
+ }
4673
+ };
4674
+ function assertGuestEnvironmentBoundary(options) {
4675
+ const refusedForwardEnv = (options.forwardEnv ?? []).filter((name) => isReservedGuestEnvironmentName(name) || options.guestCredentialMode === "host-authenticated" && !HOST_AUTHENTICATED_GUEST_ENV_ALLOWLIST.has(name));
4676
+ const refusedSandboxEnv = Object.keys(options.sandboxEnv ?? {}).filter(isReservedGuestEnvironmentName);
4677
+ const refused = [...new Set([...refusedForwardEnv, ...refusedSandboxEnv])].sort();
4678
+ if (refused.length > 0) throw new GuestEnvironmentBoundaryError(refused);
4679
+ }
4680
+ /** @deprecated Prefer assertGuestEnvironmentBoundary for mode-aware checks. */
4681
+ function assertHostAuthenticatedGuestEnvironment(options) {
4682
+ assertGuestEnvironmentBoundary({
4683
+ guestCredentialMode: "host-authenticated",
4684
+ ...options
4685
+ });
4686
+ }
4597
4687
  /**
4598
4688
  * Return whether two Gondolin hostname globs can match at least one common
4599
4689
  * string. Each `*` is an arbitrary substring, so this walks the product of the
@@ -4665,10 +4755,22 @@ async function resumeVm(config) {
4665
4755
  throwIfAborted(config.signal, "VM resume");
4666
4756
  const agentDir = resolveVmAgentDir(config);
4667
4757
  const guestWorkspace = path.resolve(config.mountPath);
4668
- if (!existsSync(agentDir)) throw new Error(`Agent directory not found: ${agentDir}. Run: moltnet register --name ${config.agentName}`);
4669
- const creds = loadCredentials(agentDir);
4670
- const moltnetConfig = JSON.parse(creds.moltnetJson);
4671
- const apiHost = new URL(moltnetConfig.endpoints.api).hostname;
4758
+ const guestCredentialMode = config.guestCredentialMode ?? "guest-config";
4759
+ if (guestCredentialMode === "guest-config" && !existsSync(agentDir)) throw new Error(`Agent directory not found: ${agentDir}. Run: moltnet register --name ${config.agentName}`);
4760
+ assertGuestEnvironmentBoundary({
4761
+ guestCredentialMode,
4762
+ forwardEnv: config.forwardEnv,
4763
+ sandboxEnv: config.sandboxConfig?.env
4764
+ });
4765
+ config.onDiagnostic?.({
4766
+ event: "vm.credentials.mode",
4767
+ level: "info",
4768
+ credentialMode: guestCredentialMode,
4769
+ message: guestCredentialMode === "host-authenticated" ? "MoltNet agent files and non-allowlisted host environment variables are withheld from the guest" : "The complete MoltNet agent configuration is available to the guest"
4770
+ });
4771
+ const creds = loadCredentials(agentDir, guestCredentialMode, config.onDiagnostic);
4772
+ const configuredApiUrl = creds.moltnetJson ? JSON.parse(creds.moltnetJson).endpoints.api : void 0;
4773
+ const apiHost = new URL(configuredApiUrl ?? process.env.MOLTNET_API_URL ?? DEFAULT_MOLTNET_API_URL).hostname;
4672
4774
  const runtimeAllowedHosts = config.sandboxConfig?.network?.allowedHosts ?? [];
4673
4775
  const runtimeAllowedInternalHosts = config.sandboxConfig?.network?.allowedInternalHosts ?? [];
4674
4776
  const protectedExternalHosts = [...new Set([
@@ -4689,7 +4791,7 @@ async function resumeVm(config) {
4689
4791
  else if (k.endsWith("_PRIVATE_KEY_PATH")) vmAgentEnv[k] = `${vmAgentDir}/${path.basename(v)}`;
4690
4792
  else vmAgentEnv[k] = v;
4691
4793
  }
4692
- vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
4794
+ if (creds.moltnetJson) vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
4693
4795
  const vfsConfig = resolveVfsShadowConfig(config.sandboxConfig);
4694
4796
  let workspaceProvider = new RealFSProvider(config.mountPath);
4695
4797
  workspaceProvider = new ShadowProvider(workspaceProvider, {
@@ -4705,6 +4807,11 @@ async function resumeVm(config) {
4705
4807
  writeMode: vfsConfig.mode
4706
4808
  });
4707
4809
  }
4810
+ if (guestCredentialMode === "host-authenticated") workspaceProvider = new ShadowProvider(workspaceProvider, {
4811
+ shouldShadow: ({ path: shadowPath }) => shadowPath.split("/").includes(".moltnet"),
4812
+ denySymlinkBypass: true,
4813
+ writeMode: "deny"
4814
+ });
4708
4815
  const forwardedEnv = {};
4709
4816
  for (const name of config.forwardEnv ?? []) {
4710
4817
  const value = process.env[name];
@@ -4777,44 +4884,47 @@ async function resumeVm(config) {
4777
4884
  if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(nonErrorMessage(lastErr));
4778
4885
  }
4779
4886
  const vmSshDir = `${vmAgentDir}/ssh`;
4780
- await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`, { signal: config.signal });
4887
+ const hasAgentFiles = guestCredentialMode === "guest-config";
4888
+ await vm.exec(hasAgentFiles ? `mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent` : "mkdir -p /home/agent/.pi/agent", { signal: config.signal });
4781
4889
  if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, {
4782
4890
  mode: 384,
4783
4891
  signal: config.signal
4784
4892
  });
4785
- const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
4786
- await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, {
4787
- mode: 384,
4788
- signal: config.signal
4789
- });
4790
- await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, {
4791
- mode: 384,
4792
- signal: config.signal
4793
- });
4794
- if (creds.gitconfig) {
4795
- const vmGitconfig = rewriteGitconfigPaths(creds.gitconfig, vmSshDir, vmAgentDir);
4796
- await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
4893
+ if (hasAgentFiles) {
4894
+ const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
4895
+ await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, {
4896
+ mode: 384,
4897
+ signal: config.signal
4898
+ });
4899
+ await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, {
4900
+ mode: 384,
4901
+ signal: config.signal
4902
+ });
4903
+ if (creds.gitconfig) {
4904
+ const vmGitconfig = rewriteGitconfigPaths(creds.gitconfig, vmSshDir, vmAgentDir);
4905
+ await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
4906
+ mode: 420,
4907
+ signal: config.signal
4908
+ });
4909
+ }
4910
+ if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, {
4911
+ mode: 384,
4912
+ signal: config.signal
4913
+ });
4914
+ if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, {
4797
4915
  mode: 420,
4798
4916
  signal: config.signal
4799
4917
  });
4918
+ if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, {
4919
+ mode: 420,
4920
+ signal: config.signal
4921
+ });
4922
+ if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, {
4923
+ mode: 384,
4924
+ signal: config.signal
4925
+ });
4800
4926
  }
4801
- if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, {
4802
- mode: 384,
4803
- signal: config.signal
4804
- });
4805
- if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, {
4806
- mode: 420,
4807
- signal: config.signal
4808
- });
4809
- if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, {
4810
- mode: 420,
4811
- signal: config.signal
4812
- });
4813
- if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, {
4814
- mode: 384,
4815
- signal: config.signal
4816
- });
4817
- await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet", { signal: config.signal });
4927
+ await vm.exec(hasAgentFiles ? "chown -R agent:agent /home/agent/.pi /home/agent/.moltnet" : "chown -R agent:agent /home/agent/.pi", { signal: config.signal });
4818
4928
  return {
4819
4929
  vm,
4820
4930
  credentials: creds,
@@ -5314,16 +5424,25 @@ function createGondolinBashOps(vm, localCwd, guestWorkspace) {
5314
5424
  * capability-aware allow-set) is tracked as future work.
5315
5425
  */
5316
5426
  function decideToolCall(input) {
5317
- if (input.enforcement === "off") return { allow: true };
5318
- if (input.toolName.startsWith("submit_") || input.toolName === "subagent") return { allow: true };
5427
+ if (input.enforcement === "off") return {
5428
+ allow: true,
5429
+ reasonCode: "policy_off"
5430
+ };
5431
+ if (input.toolName.startsWith("submit_") || input.toolName === "subagent") return {
5432
+ allow: true,
5433
+ reasonCode: "executor_protocol_tool"
5434
+ };
5319
5435
  const resolved = resolveNames(input);
5320
- if (resolved.kind === "unresolvable") return fenced(input.enforcement, "shell command could not be statically authorized", "unresolvable shell command (watch)");
5436
+ if (resolved.kind === "unresolvable") return fenced(input.enforcement, "shell_command_unresolvable", "shell command could not be statically authorized", "unresolvable shell command (watch)");
5321
5437
  const arbitraryCode = [...new Set(resolved.tools.filter((tool) => tool.risk === "arbitrary-code").map((tool) => tool.name))];
5322
- if (arbitraryCode.length > 0) return fenced(input.enforcement, `arbitrary-code interpreter not authorizable by tool policy: ${arbitraryCode.join(", ")}`, `would block — arbitrary-code interpreter (watch): ${arbitraryCode.join(", ")}`, arbitraryCode);
5438
+ if (arbitraryCode.length > 0) return fenced(input.enforcement, "arbitrary_code_interpreter", `arbitrary-code interpreter not authorizable by tool policy: ${arbitraryCode.join(", ")}`, `would block — arbitrary-code interpreter (watch): ${arbitraryCode.join(", ")}`, arbitraryCode);
5323
5439
  if (input.toolName !== "bash") {
5324
5440
  const missing = resolved.tools.map((tool) => tool.name).filter((name) => !input.allowedTools.has(name));
5325
- if (missing.length === 0) return { allow: true };
5326
- return fenced(input.enforcement, `not permitted by tool policy: ${missing.join(", ")}`, `would block (watch): ${missing.join(", ")}`, missing);
5441
+ if (missing.length === 0) return {
5442
+ allow: true,
5443
+ reasonCode: "policy_allowed"
5444
+ };
5445
+ return fenced(input.enforcement, "tool_not_permitted", `not permitted by tool policy: ${missing.join(", ")}`, `would block (watch): ${missing.join(", ")}`, missing);
5327
5446
  }
5328
5447
  const matchedShellCommands = [];
5329
5448
  const missingShellCommands = resolved.tools.filter((tool) => {
@@ -5333,7 +5452,7 @@ function decideToolCall(input) {
5333
5452
  matchedShellCommands.push(toMatchedShellCommand(tool.name, matched.argvPrefix));
5334
5453
  return false;
5335
5454
  }).map(toMissingShellCommand);
5336
- if (missingShellCommands.length === 0 && resolved.hasOutputRedirection && matchedShellCommands.length > 0) return fenced(input.enforcement, "shell output redirection requires broad executable permission", "would block shell output redirection under scoped command policy", [...new Set(matchedShellCommands.map(({ executable }) => executable))], matchedShellCommands.map(({ executable, argvPrefixFingerprint, argvPrefixLength }) => ({
5455
+ if (missingShellCommands.length === 0 && resolved.hasOutputRedirection && matchedShellCommands.length > 0) return fenced(input.enforcement, "shell_output_redirection_requires_broad_permission", "shell output redirection requires broad executable permission", "would block shell output redirection under scoped command policy", [...new Set(matchedShellCommands.map(({ executable }) => executable))], matchedShellCommands.map(({ executable, argvPrefixFingerprint, argvPrefixLength }) => ({
5337
5456
  executable,
5338
5457
  argvFingerprint: argvPrefixFingerprint,
5339
5458
  argvLength: argvPrefixLength,
@@ -5341,10 +5460,14 @@ function decideToolCall(input) {
5341
5460
  })));
5342
5461
  if (missingShellCommands.length === 0) return matchedShellCommands.length > 0 ? {
5343
5462
  allow: true,
5463
+ reasonCode: "shell_command_prefix_allowed",
5344
5464
  matchedShellCommands
5345
- } : { allow: true };
5465
+ } : {
5466
+ allow: true,
5467
+ reasonCode: "policy_allowed"
5468
+ };
5346
5469
  const missing = [...new Set(missingShellCommands.map(({ executable }) => executable))];
5347
- return fenced(input.enforcement, `not permitted by tool policy: ${missing.join(", ")}`, `would block (watch): ${missing.join(", ")}`, missing, missingShellCommands);
5470
+ return fenced(input.enforcement, "tool_not_permitted", `not permitted by tool policy: ${missing.join(", ")}`, `would block (watch): ${missing.join(", ")}`, missing, missingShellCommands);
5348
5471
  }
5349
5472
  function fingerprintArgv(argv) {
5350
5473
  return `sha256:${createHash$1("sha256").update(JSON.stringify(argv.map((token) => token === null ? { dynamic: true } : token))).digest("hex").slice(0, 16)}`;
@@ -5371,14 +5494,16 @@ function matchesArgvPrefix(argv, prefix) {
5371
5494
  * Shared enforce/watch branch: block in `enforce`, audit-and-allow in `watch`.
5372
5495
  * `enforcement` is never `off` here (short-circuited by the caller).
5373
5496
  */
5374
- function fenced(enforcement, blockReason, auditReason, missing, missingShellCommands) {
5497
+ function fenced(enforcement, reasonCode, blockReason, auditReason, missing, missingShellCommands) {
5375
5498
  if (enforcement === "enforce") return {
5376
5499
  allow: false,
5500
+ reasonCode,
5377
5501
  reason: blockReason,
5378
5502
  ...missing ? { missing } : {},
5379
5503
  ...missingShellCommands?.length ? { missingShellCommands } : {}
5380
5504
  };
5381
5505
  return {
5506
+ reasonCode,
5382
5507
  audit: auditReason,
5383
5508
  ...missing ? { missing } : {},
5384
5509
  ...missingShellCommands?.length ? { missingShellCommands } : {}
@@ -5421,8 +5546,9 @@ function resolveNames(input) {
5421
5546
  /**
5422
5547
  * Resolve the session's tool policy at start-up.
5423
5548
  *
5424
- * `off` short-circuits without a network call. Otherwise the allowed-tool set
5425
- * is fetched from the API. If that fetch fails, the mode decides the fallback:
5549
+ * The latest allowed-tool set and enforcement mode are fetched from the API,
5550
+ * including when the daemon's cached profile says `off`. If that fetch fails,
5551
+ * the cached mode decides the fallback:
5426
5552
  * `enforce` **fails closed** (empty allow-set → every non-`off` tool is
5427
5553
  * blocked); `watch` fails open (empty allow-set → every tool is audited but
5428
5554
  * allowed).
@@ -5433,12 +5559,6 @@ function resolveNames(input) {
5433
5559
  * session, stable enforcement for the run) accepted over re-fetching per call.
5434
5560
  */
5435
5561
  async function resolveSessionToolPolicy(input) {
5436
- if (input.enforcement === "off") return {
5437
- enforcement: "off",
5438
- allowedTools: /* @__PURE__ */ new Set(),
5439
- allowedShellCommands: [],
5440
- degraded: false
5441
- };
5442
5562
  const timeoutMs = input.timeoutMs ?? 5e3;
5443
5563
  try {
5444
5564
  const resolved = await withTimeout$1(input.agent.runtimeProfiles.allowedTools(input.profileId, { teamId: input.teamId }), timeoutMs);
@@ -5454,6 +5574,8 @@ async function resolveSessionToolPolicy(input) {
5454
5574
  if (rule.argvPrefix.length < 2 || rule.argvPrefix.length > 8 || rule.argvPrefix.some((token) => !token)) throw new Error("runtime returned an invalid shell command rule");
5455
5575
  return { argvPrefix: rule.argvPrefix };
5456
5576
  }),
5577
+ executionPolicySnapshotHash: resolved.policySnapshotHash,
5578
+ executionRuntimeProfileRevision: resolved.runtimeProfileRevision,
5457
5579
  degraded: false
5458
5580
  };
5459
5581
  } catch (err) {
@@ -5526,34 +5648,46 @@ function decideForEvent(event, policy, analyze) {
5526
5648
  * `off`. Register it in a session's `extensionFactories`.
5527
5649
  */
5528
5650
  function createToolPolicyExtension(deps) {
5651
+ if (deps.context?.claimPolicySnapshotHash && deps.policy.executionPolicySnapshotHash && deps.context.claimPolicySnapshotHash !== deps.policy.executionPolicySnapshotHash) deps.logger.info({
5652
+ ...decisionContext(deps),
5653
+ decision: "continue",
5654
+ reason: "claim_execution_policy_drift"
5655
+ }, "tool_policy.snapshot_drift");
5529
5656
  return (pi) => {
5530
5657
  if (deps.policy.enforcement === "off") return;
5531
5658
  pi.on("tool_call", (event) => {
5532
5659
  const decision = decideForEvent(event, deps.policy, (command) => deps.analyzer.analyze(command));
5533
5660
  if ("allow" in decision && decision.allow) {
5534
- if (decision.matchedShellCommands?.length) deps.logger.debug({
5661
+ deps.logger.info({
5662
+ ...decisionContext(deps),
5535
5663
  toolName: event.toolName,
5536
5664
  toolCallId: event.toolCallId,
5537
- matchedShellCommands: decision.matchedShellCommands
5538
- }, "tool_policy.shell_command_allowed");
5665
+ decision: "allowed",
5666
+ reason: decision.reasonCode,
5667
+ ...decision.matchedShellCommands?.length ? { shellFingerprints: decision.matchedShellCommands } : {}
5668
+ }, "tool_policy.allowed");
5539
5669
  return;
5540
5670
  }
5541
5671
  if ("audit" in decision) {
5542
5672
  deps.logger.info({
5673
+ ...decisionContext(deps),
5543
5674
  toolName: event.toolName,
5544
5675
  toolCallId: event.toolCallId,
5545
- degraded: deps.policy.degraded,
5546
- decision
5676
+ decision: "audit",
5677
+ reason: decision.reasonCode,
5678
+ ...decision.missing?.length ? { missingExecutables: decision.missing } : {},
5679
+ ...decision.missingShellCommands?.length ? { shellFingerprints: decision.missingShellCommands } : {}
5547
5680
  }, "tool_policy.audit");
5548
5681
  return;
5549
5682
  }
5550
5683
  deps.logger.warn({
5684
+ ...decisionContext(deps),
5551
5685
  toolName: event.toolName,
5552
5686
  toolCallId: event.toolCallId,
5553
- degraded: deps.policy.degraded,
5554
- reason: decision.reason,
5555
- missingExecutables: decision.missing,
5556
- missingShellCommands: decision.missingShellCommands
5687
+ decision: "blocked",
5688
+ reason: decision.reasonCode,
5689
+ ...decision.missing?.length ? { missingExecutables: decision.missing } : {},
5690
+ ...decision.missingShellCommands?.length ? { shellFingerprints: decision.missingShellCommands } : {}
5557
5691
  }, "tool_policy.blocked");
5558
5692
  return {
5559
5693
  block: true,
@@ -5562,6 +5696,15 @@ function createToolPolicyExtension(deps) {
5562
5696
  });
5563
5697
  };
5564
5698
  }
5699
+ function decisionContext(deps) {
5700
+ return {
5701
+ ...deps.context ?? {},
5702
+ enforcement: deps.policy.enforcement,
5703
+ degraded: deps.policy.degraded,
5704
+ executionPolicySnapshotHash: deps.policy.executionPolicySnapshotHash,
5705
+ executionRuntimeProfileRevision: deps.policy.executionRuntimeProfileRevision
5706
+ };
5707
+ }
5565
5708
  //#endregion
5566
5709
  //#region src/runtime/capability-discovery.ts
5567
5710
  var GuestExecutableProbeError = class extends Error {
@@ -7104,6 +7247,19 @@ var GONDOLIN_TOOL_NAMES = [
7104
7247
  "find",
7105
7248
  "grep"
7106
7249
  ];
7250
+ var HOST_AUTHENTICATED_HOST_EXEC_REFUSED_ENV = new Set([
7251
+ "GIT_CONFIG_GLOBAL",
7252
+ "MOLTNET_CREDENTIALS_PATH",
7253
+ "SSH_AUTH_SOCK"
7254
+ ]);
7255
+ function resolveHostExecBaseEnv(guestCredentialMode, agentEnv) {
7256
+ const names = new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(agentEnv)]);
7257
+ if (guestCredentialMode === "host-authenticated") {
7258
+ for (const name of HOST_AUTHENTICATED_HOST_EXEC_REFUSED_ENV) names.delete(name);
7259
+ for (const name of names) if (name.startsWith("MOLTNET_")) names.delete(name);
7260
+ }
7261
+ return names;
7262
+ }
7107
7263
  var noopTurnEventHandler = () => {};
7108
7264
  async function openVmWorkspaceFileForRead(config) {
7109
7265
  const localPath = isAbsolute(config.filePath) ? config.filePath : resolve(config.cwdPath, config.filePath);
@@ -7135,6 +7291,16 @@ function createGondolinToolDefinitions(config) {
7135
7291
  }
7136
7292
  ];
7137
7293
  }
7294
+ function createMoltNetAgentResolver(input) {
7295
+ let resolved;
7296
+ return () => {
7297
+ if (!resolved) resolved = (input.moltnetAgent ? Promise.resolve(input.moltnetAgent) : (input.connectAgent ?? ((configDir) => connect({ configDir })))(input.configDir)).catch((error) => {
7298
+ resolved = void 0;
7299
+ throw error;
7300
+ });
7301
+ return resolved;
7302
+ };
7303
+ }
7138
7304
  /**
7139
7305
  * Factory that builds a pi-specific `executeTask` function suitable for
7140
7306
  * injection into `AgentRuntime`. The returned function caches the resolved
@@ -7318,11 +7484,13 @@ async function executePiTask(claimedTask, reporter, opts) {
7318
7484
  checkpointPath,
7319
7485
  agentName: opts.agentName,
7320
7486
  agentRootDir,
7487
+ guestCredentialMode: opts.guestCredentialMode,
7321
7488
  mountPath,
7322
7489
  workspaceMode: preparedWorkspace.mode,
7323
7490
  extraAllowedHosts: opts.extraAllowedHosts,
7324
7491
  sandboxConfig: effectiveSandboxConfig,
7325
7492
  forwardEnv: opts.forwardEnv,
7493
+ onDiagnostic: opts.onVmDiagnostic,
7326
7494
  signal: reporter.cancelSignal
7327
7495
  }));
7328
7496
  } catch (err) {
@@ -7339,6 +7507,10 @@ async function executePiTask(claimedTask, reporter, opts) {
7339
7507
  activateAgentEnv(managed.credentials.agentEnv, agentRootDir);
7340
7508
  const activeWorkspace = preparedWorkspace;
7341
7509
  const activeManaged = managed;
7510
+ const getMoltNetAgent = createMoltNetAgentResolver({
7511
+ moltnetAgent: opts.moltnetAgent,
7512
+ configDir: managed.agentDir
7513
+ });
7342
7514
  await emit("info", {
7343
7515
  event: "execute_start",
7344
7516
  correlationId: task.correlationId ?? null,
@@ -7353,7 +7525,7 @@ async function executePiTask(claimedTask, reporter, opts) {
7353
7525
  let resolvedPriorContext;
7354
7526
  const continueFrom = task.input?.continueFrom;
7355
7527
  if (task.taskType === FREEFORM_TYPE && continueFrom) try {
7356
- const resolved = await resolvePriorContext(await connect({ configDir: managed.agentDir }), continueFrom);
7528
+ const resolved = await resolvePriorContext(await getMoltNetAgent(), continueFrom);
7357
7529
  if (resolved) {
7358
7530
  resolvedPriorContext = resolved;
7359
7531
  await emit("info", {
@@ -7468,7 +7640,7 @@ async function executePiTask(claimedTask, reporter, opts) {
7468
7640
  });
7469
7641
  const submitTools = submitToolDefs;
7470
7642
  try {
7471
- const moltnetAgent = await connect({ configDir: managed.agentDir });
7643
+ const moltnetAgent = await getMoltNetAgent();
7472
7644
  const moltnetTools = createMoltNetTools({
7473
7645
  getAgent: () => moltnetAgent,
7474
7646
  getDiaryId: () => diaryId,
@@ -7482,7 +7654,7 @@ async function executePiTask(claimedTask, reporter, opts) {
7482
7654
  guestWorkspace: activeManaged.guestWorkspace,
7483
7655
  filePath
7484
7656
  }),
7485
- hostExecBaseEnv: new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)]),
7657
+ hostExecBaseEnv: resolveHostExecBaseEnv(opts.guestCredentialMode ?? "guest-config", managed.credentials.agentEnv),
7486
7658
  hostExecAutoApprove: opts.hostExecAutoApprove ?? opts.sandboxConfig?.hostExec?.autoApprove ?? false,
7487
7659
  getTaskContext: () => ({
7488
7660
  taskId: task.id,
@@ -7512,28 +7684,34 @@ async function executePiTask(claimedTask, reporter, opts) {
7512
7684
  ...obj
7513
7685
  }))
7514
7686
  };
7515
- if (opts.runtimeProfileId && opts.toolEnforcement && opts.toolEnforcement !== "off") {
7516
- const [analyzer, policy] = await Promise.all([ShellCommandAnalyzer.create(), resolveSessionToolPolicy({
7687
+ const toolPolicyDecisionContext = buildToolPolicyDecisionContext(claimedTask, opts.runtimeProfileId);
7688
+ if (opts.runtimeProfileId && opts.toolEnforcement) {
7689
+ const policy = await resolveSessionToolPolicy({
7517
7690
  agent: moltnetAgent,
7518
7691
  profileId: opts.runtimeProfileId,
7519
7692
  teamId: taskTeamId,
7520
7693
  runtimeKind: opts.runtimeDefinition?.runtimeKind ?? "gondolin_pi",
7521
7694
  enforcement: opts.toolEnforcement,
7522
7695
  logger: toolPolicyLogger
7523
- })]);
7524
- verifiedGuestExecutables = (await discoverGuestExecutables(managed.vm, [...policy.allowedShellCommands.map(({ argvPrefix }) => argvPrefix[0]), ...policy.enforcement === "watch" ? resolvedVmTemplate?.executables ?? [] : []], { signal: reporter.cancelSignal })).available;
7525
- const availableExecutables = new Set(verifiedGuestExecutables);
7526
- const allowedShellCommands = policy.allowedShellCommands.filter(({ argvPrefix }) => availableExecutables.has(argvPrefix[0]));
7527
- unavailableRuntimeShellCommands = policy.allowedShellCommands.filter(({ argvPrefix }) => !availableExecutables.has(argvPrefix[0]));
7528
- resolvedToolPolicy = {
7529
- ...policy,
7530
- allowedShellCommands
7531
- };
7532
- toolPolicyExtensions.push(createToolPolicyExtension({
7533
- policy: resolvedToolPolicy,
7534
- analyzer,
7535
- logger: toolPolicyLogger
7536
- }));
7696
+ });
7697
+ resolvedToolPolicy = policy;
7698
+ if (policy.enforcement !== "off") {
7699
+ const analyzer = await ShellCommandAnalyzer.create();
7700
+ verifiedGuestExecutables = (await discoverGuestExecutables(managed.vm, [...policy.allowedShellCommands.map(({ argvPrefix }) => argvPrefix[0]), ...policy.enforcement === "watch" ? resolvedVmTemplate?.executables ?? [] : []], { signal: reporter.cancelSignal })).available;
7701
+ const availableExecutables = new Set(verifiedGuestExecutables);
7702
+ const allowedShellCommands = policy.allowedShellCommands.filter(({ argvPrefix }) => availableExecutables.has(argvPrefix[0]));
7703
+ unavailableRuntimeShellCommands = policy.allowedShellCommands.filter(({ argvPrefix }) => !availableExecutables.has(argvPrefix[0]));
7704
+ resolvedToolPolicy = {
7705
+ ...policy,
7706
+ allowedShellCommands
7707
+ };
7708
+ toolPolicyExtensions.push(createToolPolicyExtension({
7709
+ policy: resolvedToolPolicy,
7710
+ analyzer,
7711
+ logger: toolPolicyLogger,
7712
+ context: toolPolicyDecisionContext
7713
+ }));
7714
+ }
7537
7715
  }
7538
7716
  const runtimeToolContext = {
7539
7717
  agent: moltnetAgent,
@@ -7913,6 +8091,29 @@ async function executePiTask(claimedTask, reporter, opts) {
7913
8091
  });
7914
8092
  }
7915
8093
  }
8094
+ function buildToolPolicyDecisionContext(claimedTask, executionRuntimeProfileId) {
8095
+ const { task, attemptN, claimAuthority } = claimedTask;
8096
+ const proposer = task.proposedByAgentId ? {
8097
+ proposerKind: "agent",
8098
+ proposerId: task.proposedByAgentId
8099
+ } : task.proposedByHumanId ? {
8100
+ proposerKind: "human",
8101
+ proposerId: task.proposedByHumanId
8102
+ } : {};
8103
+ return {
8104
+ taskId: task.id,
8105
+ attemptN,
8106
+ teamId: task.teamId,
8107
+ ...proposer,
8108
+ ...claimAuthority?.claimantAgentId ? { claimantAgentId: claimAuthority.claimantAgentId } : {},
8109
+ ...claimAuthority?.leaseId ? { leaseId: claimAuthority.leaseId } : {},
8110
+ ...claimAuthority?.runtimeProfileId ? { claimRuntimeProfileId: claimAuthority.runtimeProfileId } : {},
8111
+ ...executionRuntimeProfileId ? { executionRuntimeProfileId } : {},
8112
+ ...typeof claimAuthority?.runtimeProfileRevision === "number" ? { claimRuntimeProfileRevision: claimAuthority.runtimeProfileRevision } : {},
8113
+ ...claimAuthority?.policySnapshotHash ? { claimPolicySnapshotHash: claimAuthority.policySnapshotHash } : {},
8114
+ ...claimAuthority?.executorFingerprint ? { claimedExecutorFingerprint: claimAuthority.executorFingerprint } : {}
8115
+ };
8116
+ }
7916
8117
  function createSessionTurnState() {
7917
8118
  return {
7918
8119
  assistantText: "",
@@ -8564,4 +8765,4 @@ function describeToolErrorMessage(result) {
8564
8765
  }
8565
8766
  }
8566
8767
  //#endregion
8567
- export { GONDOLIN_BASE_EXECUTABLES, GONDOLIN_TOOL_NAMES, HOST_EXEC_DEFAULT_BASE_ENV, MOLTNET_TOOL_NAMES, PI_EXECUTOR_MANIFEST_VERSION, PI_RUNTIME_DEFINITION_VERSION, activateAgentEnv, buildAgentSession, buildPiExecutorManifest, buildRuntimeKernel, buildWorkspaceMountInstructions, createGondolinBashOps, createGondolinEditOps, createGondolinFindOps, createGondolinLsOps, createGondolinReadOps, createGondolinToolDefinitions, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, createToolPolicyExtension, decideForEvent, decideToolCall, defineGondolinTemplate, definePiExtension, definePiRuntime, definePiTool, enabledPiToolNames, ensureSnapshot, executeGondolinGrep, executePiTask, filterModelVisibleTools, findMainWorktree, injectRuntimeContext as injectTaskContext, isKernelTool, isResolvedPathInsideRoot, isToolVisible, loadCredentials, materializePiExtensions, materializePiTools, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveSessionToolPolicy, resolveTaskWorktreePath, resumeVm, toGuestPath };
8768
+ export { GONDOLIN_BASE_EXECUTABLES, GONDOLIN_TOOL_NAMES, GuestEnvironmentBoundaryError, HOST_EXEC_DEFAULT_BASE_ENV, MOLTNET_TOOL_NAMES, PI_EXECUTOR_MANIFEST_VERSION, PI_RUNTIME_DEFINITION_VERSION, activateAgentEnv, assertGuestEnvironmentBoundary, assertHostAuthenticatedGuestEnvironment, buildAgentSession, buildPiExecutorManifest, buildRuntimeKernel, buildWorkspaceMountInstructions, createGondolinBashOps, createGondolinEditOps, createGondolinFindOps, createGondolinLsOps, createGondolinReadOps, createGondolinToolDefinitions, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, createToolPolicyExtension, decideForEvent, decideToolCall, defineGondolinTemplate, definePiExtension, definePiRuntime, definePiTool, enabledPiToolNames, ensureSnapshot, executeGondolinGrep, executePiTask, filterModelVisibleTools, findMainWorktree, injectRuntimeContext as injectTaskContext, isKernelTool, isResolvedPathInsideRoot, isToolVisible, loadCredentials, materializePiExtensions, materializePiTools, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveHostExecBaseEnv, resolveSessionToolPolicy, resolveTaskWorktreePath, resumeVm, toGuestPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-runtime",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Composable MoltNet runtime kernel for Pi agents in Gondolin VMs",
@@ -31,10 +31,10 @@
31
31
  "@opentelemetry/api": "^1.9.0",
32
32
  "multiformats": "^13.3.0",
33
33
  "typebox": "^1.2.8",
34
- "@themoltnet/agent-runtime": "0.41.2",
35
- "@themoltnet/sdk": "0.131.0",
36
- "@themoltnet/shell-command-analyzer": "0.3.0",
37
- "@themoltnet/os-keyring": "0.2.0"
34
+ "@themoltnet/agent-runtime": "0.42.0",
35
+ "@themoltnet/os-keyring": "0.2.0",
36
+ "@themoltnet/sdk": "0.133.0",
37
+ "@themoltnet/shell-command-analyzer": "0.3.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "0.79.4",
@@ -51,9 +51,9 @@
51
51
  "vite": "^8.0.0",
52
52
  "vite-plugin-dts": "^4.5.4",
53
53
  "vitest": "^3.0.0",
54
- "@moltnet/crypto-service": "0.1.0",
55
54
  "@moltnet/models": "0.1.0",
56
- "@moltnet/tasks": "0.1.0"
55
+ "@moltnet/tasks": "0.1.0",
56
+ "@moltnet/crypto-service": "0.1.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=24"