@skrr-ai/cli 0.1.49 → 0.1.51

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.
Files changed (180) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +75 -6
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +14 -7
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +9 -0
  9. package/dist/commands/agents/update.js +38 -5
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/code/index.d.ts +1 -0
  13. package/dist/commands/code/index.js +49 -5
  14. package/dist/commands/code/run.js +5 -1
  15. package/dist/commands/commitments/cycles.d.ts +19 -0
  16. package/dist/commands/commitments/cycles.js +46 -0
  17. package/dist/commands/commitments/doctor.d.ts +1 -0
  18. package/dist/commands/commitments/doctor.js +19 -4
  19. package/dist/commands/commitments/effective-policy.js +40 -1
  20. package/dist/commands/commitments/explain.d.ts +17 -0
  21. package/dist/commands/commitments/explain.js +40 -0
  22. package/dist/commands/commitments/next-tick.js +3 -1
  23. package/dist/commands/commitments/preflight.js +10 -0
  24. package/dist/commands/commitments/show.js +11 -1
  25. package/dist/commands/daemon/config.d.ts +15 -0
  26. package/dist/commands/daemon/config.js +38 -0
  27. package/dist/commands/daemon/index.js +1 -0
  28. package/dist/commands/daemon/status.js +15 -1
  29. package/dist/commands/goals/create.d.ts +1 -0
  30. package/dist/commands/goals/create.js +22 -0
  31. package/dist/commands/harnesses/models.d.ts +54 -0
  32. package/dist/commands/harnesses/models.js +100 -7
  33. package/dist/commands/harnesses/show.d.ts +11 -0
  34. package/dist/commands/harnesses/show.js +35 -0
  35. package/dist/commands/harnesses/update.d.ts +28 -1
  36. package/dist/commands/harnesses/update.js +73 -10
  37. package/dist/commands/logout.js +12 -1
  38. package/dist/commands/machines/dedicated/index.js +1 -1
  39. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  40. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  41. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  42. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  43. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  44. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  45. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  46. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  47. package/dist/commands/machines/dedicated/terminal.js +27 -2
  48. package/dist/commands/machines/hosted/connect.js +2 -2
  49. package/dist/commands/machines/hosted/destroy.js +1 -1
  50. package/dist/commands/machines/hosted/exec.js +1 -1
  51. package/dist/commands/machines/hosted/list.js +1 -1
  52. package/dist/commands/machines/hosted/pause.js +1 -1
  53. package/dist/commands/machines/hosted/pull.js +1 -1
  54. package/dist/commands/machines/hosted/resume.js +1 -1
  55. package/dist/commands/machines/hosted/start.js +2 -2
  56. package/dist/commands/machines/hosted/status.js +2 -1
  57. package/dist/commands/outbox/list.d.ts +12 -1
  58. package/dist/commands/outbox/list.js +31 -2
  59. package/dist/commands/revoke-daemon.js +9 -0
  60. package/dist/commands/spaces/create.d.ts +1 -0
  61. package/dist/commands/spaces/create.js +17 -0
  62. package/dist/commands/spaces/show.js +5 -0
  63. package/dist/commands/spaces/update.d.ts +1 -0
  64. package/dist/commands/spaces/update.js +16 -0
  65. package/dist/commands/tasks/create.d.ts +1 -0
  66. package/dist/commands/tasks/create.js +6 -0
  67. package/dist/commands/tasks/list.js +1 -0
  68. package/dist/commands/tasks/show.js +45 -0
  69. package/dist/commands/tasks/update.d.ts +1 -0
  70. package/dist/commands/tasks/update.js +22 -0
  71. package/dist/commands/tasks/upsert.d.ts +1 -0
  72. package/dist/commands/tasks/upsert.js +16 -0
  73. package/dist/lib/agent-config.d.ts +2 -0
  74. package/dist/lib/agent-config.js +3 -1
  75. package/dist/lib/agentic-stream.d.ts +68 -1
  76. package/dist/lib/agentic-stream.js +294 -7
  77. package/dist/lib/api-fetch.js +19 -0
  78. package/dist/lib/auth-core-init.d.ts +2 -10
  79. package/dist/lib/auth-core-init.js +20 -67
  80. package/dist/lib/auth-failure.d.ts +33 -0
  81. package/dist/lib/auth-failure.js +65 -0
  82. package/dist/lib/auth-storage.d.ts +14 -0
  83. package/dist/lib/auth-storage.js +14 -0
  84. package/dist/lib/commitments.d.ts +37 -2
  85. package/dist/lib/commitments.js +148 -0
  86. package/dist/lib/daemonBroker.d.ts +134 -31
  87. package/dist/lib/daemonBroker.js +376 -31
  88. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  89. package/dist/lib/dedicated-lease-command.js +30 -1
  90. package/dist/lib/dedicated-machines.js +8 -25
  91. package/dist/lib/dedicated-service-command.d.ts +11 -3
  92. package/dist/lib/dedicated-service-command.js +20 -3
  93. package/dist/lib/dedicated-service.d.ts +46 -0
  94. package/dist/lib/dedicated-service.js +85 -7
  95. package/dist/lib/dedicated-terminal.d.ts +21 -0
  96. package/dist/lib/dedicated-terminal.js +104 -9
  97. package/dist/lib/delegated-cli.js +4 -1
  98. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  99. package/dist/lib/exec-runtime-binary.js +4 -2
  100. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  101. package/dist/lib/first-party-harness-broker.js +13 -1
  102. package/dist/lib/first-party-harness-doctor.js +41 -1
  103. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  104. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  105. package/dist/lib/first-party-harness-project-trust.js +364 -0
  106. package/dist/lib/first-party-harness.d.ts +9 -2
  107. package/dist/lib/first-party-harness.js +6 -5
  108. package/dist/lib/harnesses.d.ts +10 -0
  109. package/dist/lib/hosted-machines.d.ts +10 -1
  110. package/dist/lib/hosted-machines.js +36 -2
  111. package/dist/lib/live-runtime-binary.d.ts +40 -0
  112. package/dist/lib/live-runtime-binary.js +193 -0
  113. package/dist/lib/login.d.ts +23 -0
  114. package/dist/lib/login.js +60 -10
  115. package/dist/lib/machine-spend-cap.d.ts +23 -0
  116. package/dist/lib/machine-spend-cap.js +68 -0
  117. package/dist/lib/message-intent.js +3 -1
  118. package/dist/lib/node-adapter.js +15 -3
  119. package/dist/lib/tasks.d.ts +20 -0
  120. package/dist/lib/tasks.js +54 -0
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
  128. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
  129. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  130. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  131. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  132. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  133. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  134. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  135. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  136. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  137. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  138. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  139. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  140. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  164. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  165. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  166. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  167. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  168. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  169. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
  170. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  171. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  172. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  173. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  174. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  175. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  176. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  177. package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
  178. package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
  179. package/oclif.manifest.json +24813 -24242
  180. package/package.json +4 -1
@@ -259,7 +259,10 @@ async function acquireManagedSession(input = {}, deps = {}) {
259
259
  sessionToken,
260
260
  model: input.model?.trim() || AUTOMATIC_MODEL,
261
261
  localRuntimeId: (deps.localRuntimeId ?? localRuntimeId)(),
262
- machineUuid: (deps.machineUuid ?? auth_core_1.ensureMachineDaemonId)(),
262
+ // The hardware id every Daemon row is keyed on — the same function
263
+ // the daemon calls — not the minted `~/.skrr/daemon.id` UUIDv7,
264
+ // so the reservation's attribution joins to this machine (OSK-12063).
265
+ machineUuid: (deps.machineUuid ?? auth_core_1.getMachineUuid)(),
263
266
  }, fetchImpl, acquisition.signal);
264
267
  }
265
268
  catch (err) {
@@ -999,8 +1002,15 @@ function createManagedEngineHooks(input) {
999
1002
  * is not reported to the user as a decision about their account (OSK-3961).
1000
1003
  */
1001
1004
  let tearingDown = false;
1005
+ /**
1006
+ * `prepare` answered `{}` with no credential the engine could inherit —
1007
+ * latched, so a post-exit caller can reconnect an opaque engine failure to
1008
+ * the auth cause the fallback line already named (OSK-11949).
1009
+ */
1010
+ let unprovisioned = false;
1002
1011
  return {
1003
1012
  session: () => handle,
1013
+ unprovisionedByok: () => unprovisioned,
1004
1014
  // A live handle means a broker is running and the engine's base URLs point at
1005
1015
  // it, which is exactly when re-admitting an ambient provider key would let the
1006
1016
  // engine bypass the relay (or 401 preferring a stale `x-api-key`). No handle
@@ -1012,6 +1022,7 @@ function createManagedEngineHooks(input) {
1012
1022
  if (skip) {
1013
1023
  if (skip.announce)
1014
1024
  log(`Managed inference not used: ${skip.detail}. ${fallbackTail(env)}`);
1025
+ unprovisioned = !(0, first_party_harness_1.hasProviderCredential)(env);
1015
1026
  return {};
1016
1027
  }
1017
1028
  try {
@@ -1063,6 +1074,7 @@ function createManagedEngineHooks(input) {
1063
1074
  if (failure.action === 'refuse') {
1064
1075
  throw new first_party_harness_1.EngineStartRefusedError(failure.code, failure.line);
1065
1076
  }
1077
+ unprovisioned = !(0, first_party_harness_1.hasProviderCredential)(env);
1066
1078
  return {};
1067
1079
  }
1068
1080
  },
@@ -42,6 +42,7 @@ const first_party_harness_1 = require("./first-party-harness");
42
42
  const auth_core_1 = require("@skrr-ai/auth-core");
43
43
  const harness_tiers_1 = require("./harness-tiers");
44
44
  const first_party_harness_managed_1 = require("./first-party-harness-managed");
45
+ const first_party_harness_project_trust_1 = require("./first-party-harness-project-trust");
45
46
  /**
46
47
  * Project-level state dir — the ENGINE's own project marker
47
48
  * (`FIRST_PARTY_HARNESS_ENGINE.projectDirectory`). Basename deliberately differs
@@ -490,6 +491,42 @@ function permissionAuthorityCheck(env) {
490
491
  detail: 'daemon-managed sessions adjudicate every tool call; no trust tier auto-approves',
491
492
  };
492
493
  }
494
+ /**
495
+ * OSK-11958 — whether `skrr code` will load THIS directory's project configuration.
496
+ *
497
+ * Reported because the answer changes what the engine can do before any tool
498
+ * call exists: a project's plugins and custom tools run on load, and its
499
+ * permission block can loosen what the agent does without asking. Never a
500
+ * `warn`: an untrusted project with config is the safe default working as
501
+ * designed, and one with none has nothing to decide.
502
+ */
503
+ function projectConfigCheck(cwd, env) {
504
+ const root = (0, first_party_harness_project_trust_1.findProjectRoot)(cwd);
505
+ const config = (0, first_party_harness_project_trust_1.findProjectConfig)(cwd, root, env);
506
+ if (config.length === 0) {
507
+ return {
508
+ name: 'project-config',
509
+ status: 'ok',
510
+ detail: 'this project ships no agent configuration — nothing to trust',
511
+ };
512
+ }
513
+ const listed = config.map((file) => node_path_1.default.relative(root, file) || '.').join(', ');
514
+ if ((0, first_party_harness_project_trust_1.isProjectTrusted)(root, env)) {
515
+ return {
516
+ name: 'project-config',
517
+ status: 'ok',
518
+ detail: `trusted — ${listed} load for \`skrr code\` and \`skrr code run\``,
519
+ remedy: `Recorded in ${(0, first_party_harness_project_trust_1.trustRecordPath)(env)}; delete this project's entry there to stop trusting it.`,
520
+ };
521
+ }
522
+ return {
523
+ name: 'project-config',
524
+ status: 'ok',
525
+ detail: `not trusted — ${listed} will NOT load (instructions files still do)`,
526
+ remedy: `Project configuration can run code on this machine and loosen permissions. Pass ` +
527
+ `${first_party_harness_project_trust_1.TRUST_PROJECT_FLAG} once, or answer "always" when the TUI asks, to load it.`,
528
+ };
529
+ }
493
530
  /**
494
531
  * Hosts the engine's WL-2.7 egress policy denies, and the one it still permits.
495
532
  *
@@ -759,7 +796,9 @@ function sessionAuthorityCheck(env) {
759
796
  name: 'session-authority',
760
797
  status: 'ok',
761
798
  detail: 'standalone — the engine decides. No daemon permission adjudication, no run fence, no ' +
762
- 'scoped platform credential, no durable task context, no worktree isolation. ' +
799
+ 'scoped platform credential, no durable task context, no worktree isolation. The TUI ' +
800
+ 'asks before edits, commands and web access unless OPENCODE_PERMISSION or ' + // brand-leak-ok:compat upstream env var name
801
+ '--engine-permissions says otherwise; `skrr code run` keeps the engine config. ' +
763
802
  'Daemon-supervised sessions enter through `skrr code acp`, not through this command.',
764
803
  remedy: 'Intended for local use, and nothing to fix. Worth knowing because it is NOT the same ' +
765
804
  'trust plane as a daemon-dispatched run — and neither plane is an OS-level sandbox ' +
@@ -828,6 +867,7 @@ function runDoctor(cwd = process.cwd(), env = process.env) {
828
867
  stateDirCheck(cwd, env),
829
868
  compatCheck(cwd, env),
830
869
  opencodeCheck(cwd),
870
+ projectConfigCheck(cwd, env),
831
871
  // Before the trust/permission/credential trio, because it frames all three:
832
872
  // what a tier grants and who enforces it both depend on which plane you are on.
833
873
  sessionAuthorityCheck(env),
@@ -122,9 +122,10 @@ export declare const MANAGED_SESSION_ENV = "OVERSKY_MANAGED_SESSION";
122
122
  * by `skrr` itself. Keying an AUTHORITY answer on it therefore tells a user with
123
123
  * no daemon at all that "a daemon adjudicates every tool call", which is the
124
124
  * exact conflation `sessionAuthorityCheck`'s own docstring was written to warn
125
- * about. It is not a subtle misread: bare `skrr code` never sets
126
- * `OPENCODE_PERMISSION={"*":"ask"}` (only the daemon's backend does), so the
127
- * engine consults its own allow-by-default config and nothing is adjudicated.
125
+ * about. It is not a subtle misread: nothing on this plane routes a tool call
126
+ * to the daemon. Since OSK-11958 the bare TUI asks the PERSON at the terminal
127
+ * before edits and commands (`INTERACTIVE_PERMISSION_DEFAULT`), and `skrr code
128
+ * run` keeps the engine's own config — neither is daemon adjudication.
128
129
  *
129
130
  * Ordered deliberately: DAEMON IDENTITY FIRST. Only the daemon can set
130
131
  * `OVERSKY_AUTONOMOUS_SESSION` or the runtime context a daemon session carries,
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `skrr code` on a repository somebody else wrote (OSK-11958).
3
+ *
4
+ * ── What was wrong ──────────────────────────────────────────────────────────
5
+ *
6
+ * Bare `skrr code` and `skrr code run` forwarded straight to the engine, which
7
+ * then read the project's own configuration. Three things in there act on this
8
+ * machine, and the engine's own content-addressed trust gate (`skrr code trust`)
9
+ * covers only the first:
10
+ *
11
+ * 1. `plugin` / `mcp` keys in a project config file — gated by the engine;
12
+ * 2. plugin and tool FILES auto-discovered under the project directory
13
+ * (`<dir>/plugin/*.ts`, `<dir>/tool/*.ts`), and a dependency install run in
14
+ * that directory — NOT gated, measured
15
+ * against the fork's `config.ts` / `plugin.ts` / `tool/registry.ts`;
16
+ * 3. `permission` and agent definitions, which LOOSEN what the agent may do
17
+ * without asking — findLast wins in the engine's ruleset, so a project's
18
+ * `"bash": "allow"` beats a default that asks.
19
+ *
20
+ * The daemon's managed path has always set `OPENCODE_DISABLE_PROJECT_CONFIG=1`.
21
+ * The CLI path set nothing. So opening a cloned repository with `skrr code` could
22
+ * run that repository's code, with no prompt.
23
+ *
24
+ * ── What this module decides ────────────────────────────────────────────────
25
+ *
26
+ * **Project configuration is off until the person trusts the project.** Trust is
27
+ * a record per project root (the git top level, else the directory), kept in
28
+ * `<config root>/code-trusted-projects.json`, or a one-shot `--trust-project`.
29
+ * The interactive TUI asks once, before it starts, and only when the project
30
+ * actually ships configuration; a repository with none is never asked about.
31
+ *
32
+ * Path-addressed rather than content-addressed on purpose: this is the coarse
33
+ * "is this my project?" question every editor asks (the engine's own gate stays
34
+ * content-addressed for the plugin/mcp keys inside a trusted project).
35
+ *
36
+ * **Disabling project config must not cost the repository's instructions.** The
37
+ * engine's switch also stops it reading `AGENTS.md` / `CLAUDE.md` from the
38
+ * project — text, not code — and a coding agent that ignores the repository's
39
+ * conventions is its own failure. So an untrusted run passes those files back by
40
+ * absolute path through `OPENCODE_CONFIG_CONTENT.instructions`, which the engine
41
+ * reads as plain files. Nothing executable travels that way.
42
+ *
43
+ * **The interactive TUI asks before bash, edits, and the web.** A person is at
44
+ * the terminal and the engine can prompt there, so the default is the one
45
+ * Claude Code ships: reads and searches run, changes and commands ask
46
+ * (`INTERACTIVE_PERMISSION_DEFAULT`). The engine answers "always" per session,
47
+ * so it costs one approval per kind of action, not one per call. `skrr code run`
48
+ * is headless — the engine auto-REJECTS every ask there — so it keeps the
49
+ * engine's own permission config; an `ask` default would make `run` unable to
50
+ * edit anything. A person's own `OPENCODE_PERMISSION` always wins, and
51
+ * `--engine-permissions` hands the decision back to the engine's config.
52
+ */
53
+ /** Wrapper flags this module consumes; the engine never sees them. */
54
+ export declare const TRUST_PROJECT_FLAG = "--trust-project";
55
+ export declare const ENGINE_PERMISSIONS_FLAG = "--engine-permissions";
56
+ /**
57
+ * The interactive default. Explicit keys after `*`, because the engine's ruleset
58
+ * is last-match-wins and merges this OVER the project's and the user's config:
59
+ * a key named here beats a looser value for the same key elsewhere.
60
+ */
61
+ export declare const INTERACTIVE_PERMISSION_DEFAULT: Readonly<Record<string, unknown>>;
62
+ /** The git top level containing `dir`, else `dir` itself. Never spawns git. */
63
+ export declare function findProjectRoot(dir: string): string;
64
+ /**
65
+ * Project configuration present between `cwd` and the project root — what the
66
+ * engine would load. Empty means there is nothing to decide about.
67
+ */
68
+ export declare function findProjectConfig(cwd: string, root?: string, env?: NodeJS.ProcessEnv): string[];
69
+ /**
70
+ * The project instruction files the engine would have read, by absolute path, so
71
+ * an untrusted run keeps them. Mirrors the engine: its own project directory's
72
+ * `AGENTS.md` at every level, plus the first of `AGENTS.md` / `CLAUDE.md` that
73
+ * exists anywhere between `cwd` and the root, at every level it exists.
74
+ */
75
+ export declare function findProjectInstructions(cwd: string, root?: string): string[];
76
+ export declare function trustRecordPath(env?: NodeJS.ProcessEnv): string;
77
+ export declare function isProjectTrusted(root: string, env?: NodeJS.ProcessEnv): boolean;
78
+ export declare function trustProject(root: string, env?: NodeJS.ProcessEnv): string;
79
+ export declare function takeProjectPolicyFlags(passthrough: readonly string[]): {
80
+ trustProject: boolean;
81
+ enginePermissions: boolean;
82
+ engineArgv: string[];
83
+ };
84
+ export type TrustAnswer = 'once' | 'always' | 'no';
85
+ export interface ProjectPolicyInput {
86
+ /** Where the engine will run — the TUI's positional project dir, or the cwd. */
87
+ cwd: string;
88
+ env: NodeJS.ProcessEnv;
89
+ /** `tui` asks and applies the interactive permission default; `headless` does neither. */
90
+ mode: 'tui' | 'headless';
91
+ trustProject: boolean;
92
+ enginePermissions: boolean;
93
+ /** Present only when a person can answer (both stdin and stderr are TTYs). */
94
+ ask?: (question: {
95
+ root: string;
96
+ config: string[];
97
+ }) => Promise<TrustAnswer>;
98
+ }
99
+ export interface ProjectPolicy {
100
+ /** Env the engine receives, layered over the sanitized parent. */
101
+ env: Record<string, string>;
102
+ /** Lines for stderr — what was withheld and how to change it. */
103
+ notices: string[];
104
+ projectConfig: 'loaded' | 'disabled' | 'none';
105
+ }
106
+ export declare function resolveProjectPolicy(input: ProjectPolicyInput): Promise<ProjectPolicy>;
107
+ /**
108
+ * Where the TUI will run, or null when the argv names an engine subcommand.
109
+ *
110
+ * The engine takes its subcommand FIRST (`skrr code trust --show`), and its TUI
111
+ * takes an optional project directory in that same position. So: nothing, a flag,
112
+ * or an existing directory first means the TUI; any other word is a subcommand,
113
+ * forwarded untouched — `trust` in particular must see the project config it
114
+ * exists to approve.
115
+ */
116
+ export declare function tuiProjectDir(engineArgv: readonly string[], cwd: string): string | null;
117
+ /**
118
+ * The in-terminal question, or undefined when nobody can answer it. Asked on
119
+ * stderr so a redirected stdout stays clean; anything but y/a is "no", including
120
+ * an empty line and a closed stdin.
121
+ */
122
+ export declare function ttyTrustAsker(streams?: {
123
+ input: NodeJS.ReadStream;
124
+ output: NodeJS.WriteStream;
125
+ }): ProjectPolicyInput['ask'];
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ /**
3
+ * `skrr code` on a repository somebody else wrote (OSK-11958).
4
+ *
5
+ * ── What was wrong ──────────────────────────────────────────────────────────
6
+ *
7
+ * Bare `skrr code` and `skrr code run` forwarded straight to the engine, which
8
+ * then read the project's own configuration. Three things in there act on this
9
+ * machine, and the engine's own content-addressed trust gate (`skrr code trust`)
10
+ * covers only the first:
11
+ *
12
+ * 1. `plugin` / `mcp` keys in a project config file — gated by the engine;
13
+ * 2. plugin and tool FILES auto-discovered under the project directory
14
+ * (`<dir>/plugin/*.ts`, `<dir>/tool/*.ts`), and a dependency install run in
15
+ * that directory — NOT gated, measured
16
+ * against the fork's `config.ts` / `plugin.ts` / `tool/registry.ts`;
17
+ * 3. `permission` and agent definitions, which LOOSEN what the agent may do
18
+ * without asking — findLast wins in the engine's ruleset, so a project's
19
+ * `"bash": "allow"` beats a default that asks.
20
+ *
21
+ * The daemon's managed path has always set `OPENCODE_DISABLE_PROJECT_CONFIG=1`.
22
+ * The CLI path set nothing. So opening a cloned repository with `skrr code` could
23
+ * run that repository's code, with no prompt.
24
+ *
25
+ * ── What this module decides ────────────────────────────────────────────────
26
+ *
27
+ * **Project configuration is off until the person trusts the project.** Trust is
28
+ * a record per project root (the git top level, else the directory), kept in
29
+ * `<config root>/code-trusted-projects.json`, or a one-shot `--trust-project`.
30
+ * The interactive TUI asks once, before it starts, and only when the project
31
+ * actually ships configuration; a repository with none is never asked about.
32
+ *
33
+ * Path-addressed rather than content-addressed on purpose: this is the coarse
34
+ * "is this my project?" question every editor asks (the engine's own gate stays
35
+ * content-addressed for the plugin/mcp keys inside a trusted project).
36
+ *
37
+ * **Disabling project config must not cost the repository's instructions.** The
38
+ * engine's switch also stops it reading `AGENTS.md` / `CLAUDE.md` from the
39
+ * project — text, not code — and a coding agent that ignores the repository's
40
+ * conventions is its own failure. So an untrusted run passes those files back by
41
+ * absolute path through `OPENCODE_CONFIG_CONTENT.instructions`, which the engine
42
+ * reads as plain files. Nothing executable travels that way.
43
+ *
44
+ * **The interactive TUI asks before bash, edits, and the web.** A person is at
45
+ * the terminal and the engine can prompt there, so the default is the one
46
+ * Claude Code ships: reads and searches run, changes and commands ask
47
+ * (`INTERACTIVE_PERMISSION_DEFAULT`). The engine answers "always" per session,
48
+ * so it costs one approval per kind of action, not one per call. `skrr code run`
49
+ * is headless — the engine auto-REJECTS every ask there — so it keeps the
50
+ * engine's own permission config; an `ask` default would make `run` unable to
51
+ * edit anything. A person's own `OPENCODE_PERMISSION` always wins, and
52
+ * `--engine-permissions` hands the decision back to the engine's config.
53
+ */
54
+ var __importDefault = (this && this.__importDefault) || function (mod) {
55
+ return (mod && mod.__esModule) ? mod : { "default": mod };
56
+ };
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.INTERACTIVE_PERMISSION_DEFAULT = exports.ENGINE_PERMISSIONS_FLAG = exports.TRUST_PROJECT_FLAG = void 0;
59
+ exports.findProjectRoot = findProjectRoot;
60
+ exports.findProjectConfig = findProjectConfig;
61
+ exports.findProjectInstructions = findProjectInstructions;
62
+ exports.trustRecordPath = trustRecordPath;
63
+ exports.isProjectTrusted = isProjectTrusted;
64
+ exports.trustProject = trustProject;
65
+ exports.takeProjectPolicyFlags = takeProjectPolicyFlags;
66
+ exports.resolveProjectPolicy = resolveProjectPolicy;
67
+ exports.tuiProjectDir = tuiProjectDir;
68
+ exports.ttyTrustAsker = ttyTrustAsker;
69
+ const node_fs_1 = __importDefault(require("node:fs"));
70
+ const node_path_1 = __importDefault(require("node:path"));
71
+ const node_readline_1 = __importDefault(require("node:readline"));
72
+ const auth_core_1 = require("@skrr-ai/auth-core");
73
+ /** Wrapper flags this module consumes; the engine never sees them. */
74
+ exports.TRUST_PROJECT_FLAG = '--trust-project';
75
+ exports.ENGINE_PERMISSIONS_FLAG = '--engine-permissions';
76
+ // The upstream engine's variable names, which the fork still reads.
77
+ const DISABLE_PROJECT_CONFIG_ENV = 'OPENCODE_DISABLE_PROJECT_CONFIG'; // brand-leak-ok:compat upstream env var name
78
+ const PERMISSION_ENV = 'OPENCODE_PERMISSION'; // brand-leak-ok:compat upstream env var name
79
+ const CONFIG_CONTENT_ENV = 'OPENCODE_CONFIG_CONTENT'; // brand-leak-ok:compat upstream env var name
80
+ /**
81
+ * The upstream project namespace, which the engine reads ONLY when its
82
+ * compatibility switch is on — otherwise a repository with an upstream config
83
+ * directory would be asked about a file the engine never opens.
84
+ */
85
+ const UPSTREAM_APP_NAME = 'opencode'; // brand-leak-ok:compat upstream project dir / config basename
86
+ function upstreamCompatOn(env) {
87
+ const value = (0, auth_core_1.readFirstPartyHarnessEngineEnv)(env, 'compatOpencode').value?.trim().toLowerCase();
88
+ return value === '1' || value === 'true';
89
+ }
90
+ /**
91
+ * The interactive default. Explicit keys after `*`, because the engine's ruleset
92
+ * is last-match-wins and merges this OVER the project's and the user's config:
93
+ * a key named here beats a looser value for the same key elsewhere.
94
+ */
95
+ exports.INTERACTIVE_PERMISSION_DEFAULT = Object.freeze({
96
+ '*': 'ask',
97
+ read: { '*': 'allow', '*.env': 'ask', '*.env.*': 'ask', '*.env.example': 'allow' },
98
+ glob: 'allow',
99
+ grep: 'allow',
100
+ list: 'allow',
101
+ lsp: 'allow',
102
+ todowrite: 'allow',
103
+ question: 'allow',
104
+ skill: 'allow',
105
+ task: 'allow',
106
+ edit: 'ask',
107
+ bash: 'ask',
108
+ webfetch: 'ask',
109
+ websearch: 'ask',
110
+ external_directory: 'ask',
111
+ });
112
+ /** Every project-config location the engine reads, current namespace first. */
113
+ function projectConfigNames(env) {
114
+ const upstream = upstreamCompatOn(env) ? [UPSTREAM_APP_NAME] : [];
115
+ const appNames = [
116
+ auth_core_1.FIRST_PARTY_HARNESS_ENGINE.appName,
117
+ ...auth_core_1.FIRST_PARTY_HARNESS_ENGINE.legacyAppNames,
118
+ ...upstream,
119
+ ];
120
+ return {
121
+ dirs: [
122
+ auth_core_1.FIRST_PARTY_HARNESS_ENGINE.projectDirectory,
123
+ ...auth_core_1.FIRST_PARTY_HARNESS_ENGINE.legacyProjectDirectories,
124
+ ...upstream.map((name) => `.${name}`),
125
+ ],
126
+ files: appNames.flatMap((name) => [`${name}.json`, `${name}.jsonc`]),
127
+ };
128
+ }
129
+ /** The git top level containing `dir`, else `dir` itself. Never spawns git. */
130
+ function findProjectRoot(dir) {
131
+ let current = node_path_1.default.resolve(dir);
132
+ for (;;) {
133
+ if (node_fs_1.default.existsSync(node_path_1.default.join(current, '.git')))
134
+ return current;
135
+ const parent = node_path_1.default.dirname(current);
136
+ if (parent === current)
137
+ return node_path_1.default.resolve(dir);
138
+ current = parent;
139
+ }
140
+ }
141
+ /** The directories from `start` up to and including `root`. */
142
+ function ancestry(start, root) {
143
+ const out = [];
144
+ let current = node_path_1.default.resolve(start);
145
+ const stop = node_path_1.default.resolve(root);
146
+ for (;;) {
147
+ out.push(current);
148
+ if (current === stop)
149
+ break;
150
+ const parent = node_path_1.default.dirname(current);
151
+ if (parent === current)
152
+ break;
153
+ current = parent;
154
+ }
155
+ return out;
156
+ }
157
+ /**
158
+ * Project configuration present between `cwd` and the project root — what the
159
+ * engine would load. Empty means there is nothing to decide about.
160
+ */
161
+ function findProjectConfig(cwd, root = findProjectRoot(cwd), env = process.env) {
162
+ const { dirs, files } = projectConfigNames(env);
163
+ const found = [];
164
+ for (const dir of ancestry(cwd, root)) {
165
+ for (const name of [...dirs, ...files]) {
166
+ const candidate = node_path_1.default.join(dir, name);
167
+ if (node_fs_1.default.existsSync(candidate))
168
+ found.push(candidate);
169
+ }
170
+ }
171
+ return found;
172
+ }
173
+ /**
174
+ * The project instruction files the engine would have read, by absolute path, so
175
+ * an untrusted run keeps them. Mirrors the engine: its own project directory's
176
+ * `AGENTS.md` at every level, plus the first of `AGENTS.md` / `CLAUDE.md` that
177
+ * exists anywhere between `cwd` and the root, at every level it exists.
178
+ */
179
+ function findProjectInstructions(cwd, root = findProjectRoot(cwd)) {
180
+ const levels = ancestry(cwd, root);
181
+ const own = [
182
+ auth_core_1.FIRST_PARTY_HARNESS_ENGINE.projectDirectory,
183
+ ...auth_core_1.FIRST_PARTY_HARNESS_ENGINE.legacyProjectDirectories,
184
+ ];
185
+ const found = [];
186
+ for (const dir of levels) {
187
+ // Current project directory first; a legacy one only where it is absent.
188
+ const hit = own.map((d) => node_path_1.default.join(dir, d, 'AGENTS.md')).find((f) => node_fs_1.default.existsSync(f));
189
+ if (hit)
190
+ found.push(hit);
191
+ }
192
+ for (const family of ['AGENTS.md', 'CLAUDE.md']) {
193
+ const matches = levels.map((dir) => node_path_1.default.join(dir, family)).filter((f) => node_fs_1.default.existsSync(f));
194
+ if (matches.length > 0) {
195
+ found.push(...matches);
196
+ break;
197
+ }
198
+ }
199
+ return found;
200
+ }
201
+ function trustRecordPath(env = process.env) {
202
+ return node_path_1.default.join((0, auth_core_1.resolveConfigRoot)(env), 'code-trusted-projects.json');
203
+ }
204
+ function realRoot(root) {
205
+ try {
206
+ return node_fs_1.default.realpathSync(root);
207
+ }
208
+ catch {
209
+ return node_path_1.default.resolve(root);
210
+ }
211
+ }
212
+ function readTrustFile(file) {
213
+ try {
214
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
215
+ if (parsed && parsed.version === 1 && parsed.projects && typeof parsed.projects === 'object') {
216
+ return { version: 1, projects: parsed.projects };
217
+ }
218
+ }
219
+ catch {
220
+ /* absent or unreadable: nothing is trusted, which is the safe reading */
221
+ }
222
+ return { version: 1, projects: {} };
223
+ }
224
+ function isProjectTrusted(root, env = process.env) {
225
+ return Boolean(readTrustFile(trustRecordPath(env)).projects[realRoot(root)]);
226
+ }
227
+ function trustProject(root, env = process.env) {
228
+ const file = trustRecordPath(env);
229
+ const record = readTrustFile(file);
230
+ record.projects[realRoot(root)] = { trustedAt: new Date().toISOString() };
231
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true, mode: 0o700 });
232
+ const tmp = `${file}.${process.pid}.tmp`;
233
+ node_fs_1.default.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
234
+ node_fs_1.default.renameSync(tmp, file);
235
+ return file;
236
+ }
237
+ // ── Flags ──────────────────────────────────────────────────────────────────
238
+ function takeProjectPolicyFlags(passthrough) {
239
+ let trust = false;
240
+ let enginePermissions = false;
241
+ const engineArgv = [];
242
+ for (const token of passthrough) {
243
+ if (token === exports.TRUST_PROJECT_FLAG)
244
+ trust = true;
245
+ else if (token === exports.ENGINE_PERMISSIONS_FLAG)
246
+ enginePermissions = true;
247
+ else
248
+ engineArgv.push(token);
249
+ }
250
+ return { trustProject: trust, enginePermissions, engineArgv };
251
+ }
252
+ function mergeConfigContent(existing, instructions) {
253
+ let base = {};
254
+ if (existing && existing.trim() !== '') {
255
+ try {
256
+ const parsed = JSON.parse(existing);
257
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
258
+ return null;
259
+ base = parsed;
260
+ }
261
+ catch {
262
+ // The person's own value is not JSON we can extend; leave it exactly as is.
263
+ return null;
264
+ }
265
+ }
266
+ const prior = Array.isArray(base.instructions) ? base.instructions : [];
267
+ const merged = [...prior, ...instructions.filter((file) => !prior.includes(file))];
268
+ return JSON.stringify({ ...base, instructions: merged });
269
+ }
270
+ function relativeList(root, files) {
271
+ return files.map((file) => node_path_1.default.relative(root, file) || '.').join(', ');
272
+ }
273
+ async function resolveProjectPolicy(input) {
274
+ const env = {};
275
+ const notices = [];
276
+ if (input.mode === 'tui' && !input.enginePermissions && !input.env[PERMISSION_ENV]) {
277
+ env[PERMISSION_ENV] = JSON.stringify(exports.INTERACTIVE_PERMISSION_DEFAULT);
278
+ }
279
+ const root = findProjectRoot(input.cwd);
280
+ const config = findProjectConfig(input.cwd, root, input.env);
281
+ if (config.length === 0)
282
+ return { env, notices, projectConfig: 'none' };
283
+ let trusted = input.trustProject || isProjectTrusted(root, input.env);
284
+ if (!trusted && input.mode === 'tui' && input.ask) {
285
+ const answer = await input.ask({ root, config });
286
+ if (answer === 'always') {
287
+ const file = trustProject(root, input.env);
288
+ notices.push(`Trusted ${root} for skrr code (recorded in ${file}).`);
289
+ }
290
+ trusted = answer !== 'no';
291
+ }
292
+ if (trusted)
293
+ return { env, notices, projectConfig: 'loaded' };
294
+ env[DISABLE_PROJECT_CONFIG_ENV] = '1';
295
+ const instructions = findProjectInstructions(input.cwd, root);
296
+ if (instructions.length > 0) {
297
+ const content = mergeConfigContent(input.env[CONFIG_CONTENT_ENV], instructions);
298
+ if (content)
299
+ env[CONFIG_CONTENT_ENV] = content;
300
+ }
301
+ notices.push(`Project configuration in ${root} was not loaded (${relativeList(root, config)}): it can run ` +
302
+ `code on this machine and change what the agent may do without asking. ` +
303
+ (instructions.length > 0 ? 'Its instructions files were still read. ' : '') +
304
+ `To load it, pass ${exports.TRUST_PROJECT_FLAG} once, or answer "always" when \`skrr code\` asks.`);
305
+ return { env, notices, projectConfig: 'disabled' };
306
+ }
307
+ /**
308
+ * Where the TUI will run, or null when the argv names an engine subcommand.
309
+ *
310
+ * The engine takes its subcommand FIRST (`skrr code trust --show`), and its TUI
311
+ * takes an optional project directory in that same position. So: nothing, a flag,
312
+ * or an existing directory first means the TUI; any other word is a subcommand,
313
+ * forwarded untouched — `trust` in particular must see the project config it
314
+ * exists to approve.
315
+ */
316
+ function tuiProjectDir(engineArgv, cwd) {
317
+ const first = engineArgv[0];
318
+ if (first === undefined || first.startsWith('-'))
319
+ return cwd;
320
+ const candidate = node_path_1.default.resolve(cwd, first);
321
+ try {
322
+ if (node_fs_1.default.statSync(candidate).isDirectory())
323
+ return candidate;
324
+ }
325
+ catch {
326
+ /* not a directory: a subcommand */
327
+ }
328
+ return null;
329
+ }
330
+ /**
331
+ * The in-terminal question, or undefined when nobody can answer it. Asked on
332
+ * stderr so a redirected stdout stays clean; anything but y/a is "no", including
333
+ * an empty line and a closed stdin.
334
+ */
335
+ function ttyTrustAsker(streams = {
336
+ input: process.stdin,
337
+ output: process.stderr,
338
+ }) {
339
+ if (!streams.input.isTTY || !streams.output.isTTY)
340
+ return undefined;
341
+ return ({ root, config }) => new Promise((resolve) => {
342
+ const rl = node_readline_1.default.createInterface({ input: streams.input, output: streams.output });
343
+ let settled = false;
344
+ const finish = (answer) => {
345
+ if (settled)
346
+ return;
347
+ settled = true;
348
+ rl.close();
349
+ resolve(answer);
350
+ };
351
+ rl.on('close', () => finish('no'));
352
+ rl.question(`\nThis project ships its own agent configuration: ${relativeList(root, config)}\n` +
353
+ 'Project configuration can run code on this machine (plugins, custom tools, MCP\n' +
354
+ 'servers) and change what the agent may do without asking.\n' +
355
+ `Load it? [y] this time [a] always for ${root} [N] no `, (reply) => {
356
+ const value = reply.trim().toLowerCase();
357
+ finish(value === 'a' || value === 'always'
358
+ ? 'always'
359
+ : value === 'y' || value === 'yes'
360
+ ? 'once'
361
+ : 'no');
362
+ });
363
+ });
364
+ }
@@ -264,6 +264,13 @@ export interface EngineSpawnOptions {
264
264
  * never spawned, `cleanup` still runs, and the error reaches the caller.
265
265
  */
266
266
  prepare?: () => Promise<Record<string, string>>;
267
+ /**
268
+ * OSK-11958 — the project policy's env (`first-party-harness-project-trust.ts`):
269
+ * whether the project's own config loads, and the interactive permission
270
+ * default. Layered over the engine-owned env and UNDER `prepare`'s, so a managed
271
+ * session's relay values still win.
272
+ */
273
+ policyEnv?: Record<string, string>;
267
274
  /** Always run, on every exit path. Tear down anything `prepare` created. */
268
275
  cleanup?: (outcome: EngineExitOutcome) => Promise<void>;
269
276
  /**
@@ -367,9 +374,9 @@ export declare function strippedProviderCredentialNames(env?: NodeJS.ProcessEnv)
367
374
  * under that name, rather than rely on inheritance.
368
375
  *
369
376
  * The layering, in full: sanitized parent → the user's own model credential (BYOK
370
- * only) → {@link engineOwnedEnv} → `extraEnv`.
377
+ * only) → {@link engineOwnedEnv} → the project policy (OSK-11958) → `extraEnv`.
371
378
  */
372
- export declare function engineSpawnEnv(env: NodeJS.ProcessEnv, extraEnv: Record<string, string>, mode: 'managed' | 'byok'): NodeJS.ProcessEnv;
379
+ export declare function engineSpawnEnv(env: NodeJS.ProcessEnv, extraEnv: Record<string, string>, mode: 'managed' | 'byok', policyEnv?: Record<string, string>): NodeJS.ProcessEnv;
373
380
  /**
374
381
  * Settings the platform reads under NEUTRAL names, handed to the engine under the
375
382
  * engine's OWN names.