@skrr-ai/cli 0.1.49 → 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.
- package/README.md +5 -4
- package/dist/base-command.js +16 -4
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/update.js +11 -5
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +49 -5
- package/dist/commands/code/run.js +5 -1
- package/dist/commands/commitments/cycles.d.ts +19 -0
- package/dist/commands/commitments/cycles.js +46 -0
- package/dist/commands/commitments/effective-policy.js +26 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- package/dist/commands/logout.js +12 -1
- package/dist/commands/machines/dedicated/index.js +1 -1
- package/dist/commands/machines/dedicated/sign-in.js +4 -1
- package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
- package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
- package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
- package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
- package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
- package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
- package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
- package/dist/commands/machines/dedicated/terminal.js +27 -2
- package/dist/commands/machines/hosted/connect.js +2 -2
- package/dist/commands/machines/hosted/destroy.js +1 -1
- package/dist/commands/machines/hosted/exec.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/pause.js +1 -1
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/resume.js +1 -1
- package/dist/commands/machines/hosted/start.js +2 -2
- package/dist/commands/machines/hosted/status.js +2 -1
- package/dist/commands/tasks/create.js +1 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/update.js +1 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +17 -2
- package/dist/lib/commitments.js +114 -0
- package/dist/lib/daemonBroker.d.ts +120 -31
- package/dist/lib/daemonBroker.js +313 -20
- package/dist/lib/dedicated-lease-command.d.ts +14 -0
- package/dist/lib/dedicated-lease-command.js +30 -1
- package/dist/lib/dedicated-machines.js +8 -25
- package/dist/lib/dedicated-service-command.d.ts +11 -3
- package/dist/lib/dedicated-service-command.js +20 -3
- package/dist/lib/dedicated-service.d.ts +46 -0
- package/dist/lib/dedicated-service.js +85 -7
- package/dist/lib/dedicated-terminal.d.ts +21 -0
- package/dist/lib/dedicated-terminal.js +104 -9
- package/dist/lib/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +9 -0
- package/dist/lib/first-party-harness-doctor.js +41 -1
- package/dist/lib/first-party-harness-managed.d.ts +4 -3
- package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
- package/dist/lib/first-party-harness-project-trust.js +364 -0
- package/dist/lib/first-party-harness.d.ts +9 -2
- package/dist/lib/first-party-harness.js +6 -5
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/login.js +16 -0
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/node-adapter.js +15 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +109 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +171 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +109 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +164 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +40 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3413 -3343
- package/oclif.manifest.json +15769 -15342
- 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
|
|
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
|
-
|
|
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', {
|