@skrr-ai/cli 0.1.48 → 0.1.50

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 (117) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +16 -4
  3. package/dist/commands/agent-worker.d.ts +12 -0
  4. package/dist/commands/agent-worker.js +140 -0
  5. package/dist/commands/agents/create.js +14 -7
  6. package/dist/commands/agents/update.js +11 -5
  7. package/dist/commands/code/index.d.ts +1 -0
  8. package/dist/commands/code/index.js +49 -5
  9. package/dist/commands/code/run.js +5 -1
  10. package/dist/commands/commitments/cycles.d.ts +19 -0
  11. package/dist/commands/commitments/cycles.js +46 -0
  12. package/dist/commands/commitments/effective-policy.js +26 -1
  13. package/dist/commands/commitments/explain.d.ts +17 -0
  14. package/dist/commands/commitments/explain.js +40 -0
  15. package/dist/commands/goals/create.d.ts +1 -0
  16. package/dist/commands/goals/create.js +22 -0
  17. package/dist/commands/logout.js +12 -1
  18. package/dist/commands/machines/dedicated/index.js +1 -1
  19. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  20. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  21. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  22. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  23. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  24. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  25. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  26. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  27. package/dist/commands/machines/dedicated/terminal.js +27 -2
  28. package/dist/commands/machines/hosted/connect.js +2 -2
  29. package/dist/commands/machines/hosted/destroy.js +1 -1
  30. package/dist/commands/machines/hosted/exec.js +1 -1
  31. package/dist/commands/machines/hosted/list.js +1 -1
  32. package/dist/commands/machines/hosted/pause.js +1 -1
  33. package/dist/commands/machines/hosted/pull.js +1 -1
  34. package/dist/commands/machines/hosted/resume.js +1 -1
  35. package/dist/commands/machines/hosted/start.js +2 -2
  36. package/dist/commands/machines/hosted/status.js +2 -1
  37. package/dist/commands/tasks/create.js +1 -0
  38. package/dist/commands/tasks/list.js +1 -0
  39. package/dist/commands/tasks/update.js +1 -0
  40. package/dist/lib/agent-config.d.ts +2 -0
  41. package/dist/lib/agent-config.js +3 -1
  42. package/dist/lib/api-fetch.js +19 -0
  43. package/dist/lib/auth-storage.d.ts +14 -0
  44. package/dist/lib/auth-storage.js +14 -0
  45. package/dist/lib/commitments.d.ts +17 -2
  46. package/dist/lib/commitments.js +114 -0
  47. package/dist/lib/daemon-installer.d.ts +6 -5
  48. package/dist/lib/daemon-installer.js +4 -17
  49. package/dist/lib/daemonBroker.d.ts +120 -31
  50. package/dist/lib/daemonBroker.js +313 -20
  51. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  52. package/dist/lib/dedicated-lease-command.js +30 -1
  53. package/dist/lib/dedicated-machines.js +8 -25
  54. package/dist/lib/dedicated-service-command.d.ts +11 -3
  55. package/dist/lib/dedicated-service-command.js +20 -3
  56. package/dist/lib/dedicated-service.d.ts +46 -0
  57. package/dist/lib/dedicated-service.js +85 -7
  58. package/dist/lib/dedicated-terminal.d.ts +21 -0
  59. package/dist/lib/dedicated-terminal.js +104 -9
  60. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  61. package/dist/lib/first-party-harness-broker.js +9 -0
  62. package/dist/lib/first-party-harness-doctor.js +41 -1
  63. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  64. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  65. package/dist/lib/first-party-harness-project-trust.js +364 -0
  66. package/dist/lib/first-party-harness.d.ts +9 -2
  67. package/dist/lib/first-party-harness.js +6 -5
  68. package/dist/lib/hosted-machines.d.ts +10 -1
  69. package/dist/lib/hosted-machines.js +36 -2
  70. package/dist/lib/login.js +16 -0
  71. package/dist/lib/machine-spend-cap.d.ts +23 -0
  72. package/dist/lib/machine-spend-cap.js +68 -0
  73. package/dist/lib/node-adapter.js +15 -3
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +109 -0
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +171 -0
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +1 -1
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +1 -1
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -2
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -9
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +38 -0
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +66 -1
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +109 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +164 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +1 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +1 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -9
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +38 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +64 -0
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  114. package/dist/node_modules/@skrr-ai/auth-core/package.json +41 -1
  115. package/dist/node_modules/@skrr-ai/data-provider/index.js +3413 -3343
  116. package/oclif.manifest.json +8930 -8466
  117. package/package.json +4 -1
@@ -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.
@@ -576,16 +576,17 @@ function strippedProviderCredentialNames(env = process.env) {
576
576
  * under that name, rather than rely on inheritance.
577
577
  *
578
578
  * The layering, in full: sanitized parent → the user's own model credential (BYOK
579
- * only) → {@link engineOwnedEnv} → `extraEnv`.
579
+ * only) → {@link engineOwnedEnv} → the project policy (OSK-11958) → `extraEnv`.
580
580
  */
581
581
  function engineSpawnEnv(env, extraEnv,
582
582
  // Required, not defaulted: the safe value differs per call and a parameter that
583
583
  // can be forgotten on a security boundary will be.
584
- mode) {
584
+ mode, policyEnv = {}) {
585
585
  const spawnEnv = {
586
586
  ...(0, auth_core_1.sanitizeSpawnEnv)(env),
587
587
  ...(mode === 'managed' ? {} : inheritedProviderCredentials(env)),
588
588
  ...engineOwnedEnv(env),
589
+ ...policyEnv,
589
590
  ...extraEnv,
590
591
  };
591
592
  if (mode !== 'managed')
@@ -671,7 +672,7 @@ async function execEngine(args, env = process.env, options = {}) {
671
672
  const spawnArgs = options.finalizeArgs ? options.finalizeArgs([...args]) : args;
672
673
  // Read AFTER `prepare`, because that is when the hook knows what it acquired.
673
674
  const mode = options.credentialMode?.() ?? 'byok';
674
- return await spawnEngine(bin, spawnArgs, env, extraEnv, mode, outcome);
675
+ return await spawnEngine(bin, spawnArgs, env, extraEnv, mode, outcome, options.policyEnv);
675
676
  }
676
677
  finally {
677
678
  // In a `finally` rather than on `exit`, so a spawn error and a Ctrl-C both
@@ -683,12 +684,12 @@ async function execEngine(args, env = process.env, options = {}) {
683
684
  function spawnEngine(bin, args, env, extraEnv, mode,
684
685
  // Written into rather than returned, so the `finally` above can read it on
685
686
  // every path — including one where this promise rejects.
686
- outcome) {
687
+ outcome, policyEnv) {
687
688
  return new Promise((resolve, reject) => {
688
689
  const isWinShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin);
689
690
  const child = (0, node_child_process_1.spawn)(bin, args, {
690
691
  stdio: 'inherit',
691
- env: engineSpawnEnv(env, extraEnv, mode),
692
+ env: engineSpawnEnv(env, extraEnv, mode, policyEnv),
692
693
  ...(isWinShim ? { shell: true } : {}),
693
694
  });
694
695
  // `stdio: 'inherit'` puts the child in this process group, so Ctrl-C reaches
@@ -124,13 +124,22 @@ export declare function hostedMachineStatus(session: HostedMachineSession | unde
124
124
  export declare function hostedMachineDisplayName(session: HostedMachineSession | undefined | null): string;
125
125
  export declare function hostedMachineRepoSummary(session: HostedMachineSession | undefined | null): string;
126
126
  export declare function hostedMachineErrorSummary(session: HostedMachineSession | undefined | null): string;
127
+ /**
128
+ * Whether tenant code on this machine can read the daemon's credentials
129
+ * (OSK-11981). The server always sends a mode — a machine with no record is
130
+ * reported as `shared_uid` — so a missing field here means an older server,
131
+ * and is printed as unknown rather than as the reassuring answer.
132
+ */
133
+ export declare function hostedMachineIsolationSummary(session: HostedMachineSession | undefined | null): string;
127
134
  /** Stable, user-facing snapshot for long provisioning waits. */
128
135
  export declare function hostedMachineProgressSummary(session: HostedMachineSession | undefined | null): string;
129
136
  export declare function isHostedMachineReady(session: HostedMachineSession | undefined | null): boolean;
130
137
  export declare function isHostedMachineTerminal(session: HostedMachineSession | undefined | null): boolean;
131
138
  export declare function parseDurationMs(value: string | undefined, flagName: string): number | undefined;
132
139
  export declare function sleep(ms: number): Promise<void>;
133
- export declare function formatHostedMachineApiError(err: unknown): string | null;
140
+ export declare function formatHostedMachineApiError(err: unknown, context?: {
141
+ bin?: string;
142
+ }): string | null;
134
143
  export declare function startHostedMachine(input: StartHostedMachineInput): Promise<HostedMachineResponse>;
135
144
  export declare function listHostedMachines(scope?: HostedMachineScope & {
136
145
  limit?: number;
@@ -8,6 +8,7 @@ exports.hostedMachineStatus = hostedMachineStatus;
8
8
  exports.hostedMachineDisplayName = hostedMachineDisplayName;
9
9
  exports.hostedMachineRepoSummary = hostedMachineRepoSummary;
10
10
  exports.hostedMachineErrorSummary = hostedMachineErrorSummary;
11
+ exports.hostedMachineIsolationSummary = hostedMachineIsolationSummary;
11
12
  exports.hostedMachineProgressSummary = hostedMachineProgressSummary;
12
13
  exports.isHostedMachineReady = isHostedMachineReady;
13
14
  exports.isHostedMachineTerminal = isHostedMachineTerminal;
@@ -26,6 +27,7 @@ exports.waitForHostedMachineReady = waitForHostedMachineReady;
26
27
  exports.offlineForLabel = offlineForLabel;
27
28
  const api_fetch_1 = require("./api-fetch");
28
29
  const edge_error_page_1 = require("./edge-error-page");
30
+ const machine_spend_cap_1 = require("./machine-spend-cap");
29
31
  function curateHostedMachineList(response) {
30
32
  return {
31
33
  object: 'list',
@@ -107,6 +109,21 @@ function hostedMachineErrorSummary(session) {
107
109
  return error;
108
110
  return error.message || error.class || '';
109
111
  }
112
+ /**
113
+ * Whether tenant code on this machine can read the daemon's credentials
114
+ * (OSK-11981). The server always sends a mode — a machine with no record is
115
+ * reported as `shared_uid` — so a missing field here means an older server,
116
+ * and is printed as unknown rather than as the reassuring answer.
117
+ */
118
+ function hostedMachineIsolationSummary(session) {
119
+ const isolation = session?.identityIsolation;
120
+ if (!isolation?.mode)
121
+ return 'unknown (server did not report it)';
122
+ if (isolation.mode === 'daemon_workload_split') {
123
+ return `daemon credentials isolated from tenant code (contract ${isolation.contract || '?'})`;
124
+ }
125
+ return `shared uid — tenant code can read the daemon's credentials (${isolation.reason || 'unspecified'})`;
126
+ }
110
127
  /** Stable, user-facing snapshot for long provisioning waits. */
111
128
  function hostedMachineProgressSummary(session) {
112
129
  const status = hostedMachineStatus(session);
@@ -142,7 +159,7 @@ function parseDurationMs(value, flagName) {
142
159
  function sleep(ms) {
143
160
  return new Promise((resolve) => setTimeout(resolve, ms));
144
161
  }
145
- function formatHostedMachineApiError(err) {
162
+ function formatHostedMachineApiError(err, context = {}) {
146
163
  if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
147
164
  return null;
148
165
  }
@@ -155,13 +172,30 @@ function formatHostedMachineApiError(err) {
155
172
  if (edge) {
156
173
  return edge.message;
157
174
  }
175
+ let message;
158
176
  try {
159
177
  const parsed = JSON.parse(err.body);
160
- return parsed.message || parsed.error || parsed.code || null;
178
+ message = parsed.message || parsed.error || parsed.code || null;
161
179
  }
162
180
  catch {
163
181
  return err.body.trim() || null;
164
182
  }
183
+ // A monthly-cap refusal carries its figures in `details`; print them, and say
184
+ // this is not the plan or the credits (OSK-11946). The dedicated CLI has done
185
+ // the figures since OSK-9552; the hosted one dropped them.
186
+ if (message && (0, machine_spend_cap_1.isMonthlyCapRefusal)(err.body)) {
187
+ const figures = (0, machine_spend_cap_1.describeMonthlyCapFigures)(err.body);
188
+ return [
189
+ message,
190
+ figures ? `${figures}.` : '',
191
+ machine_spend_cap_1.MONTHLY_CAP_IS_SEPARATE,
192
+ "Start with a higher --spending-limit-cents, or raise an existing machine's cap with " +
193
+ `\`${context.bin || 'skrr'} harnesses leases billing <lease-id> --spending-limit-cents <n>\`.`,
194
+ ]
195
+ .filter(Boolean)
196
+ .join(' ');
197
+ }
198
+ return message;
165
199
  }
166
200
  async function startHostedMachine(input) {
167
201
  return (0, api_fetch_1.apiFetch)('/api/agentic/sandbox-sessions', {