@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.
- package/README.md +5 -4
- package/dist/base-command.js +75 -6
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +9 -0
- package/dist/commands/agents/update.js +38 -5
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- 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/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +40 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/commitments/next-tick.js +3 -1
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +38 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- 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/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +1 -0
- package/dist/commands/tasks/create.js +6 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +22 -0
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/agentic-stream.d.ts +68 -1
- package/dist/lib/agentic-stream.js +294 -7
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +65 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +37 -2
- package/dist/lib/commitments.js +148 -0
- package/dist/lib/daemonBroker.d.ts +134 -31
- package/dist/lib/daemonBroker.js +376 -31
- 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/delegated-cli.js +4 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +13 -1
- 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/harnesses.d.ts +10 -0
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +60 -10
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/node-adapter.js +15 -3
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +54 -0
- 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/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- 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 +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -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.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
- 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/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- 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/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- 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 +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -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.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
- 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/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- 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 +61 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
- package/oclif.manifest.json +24813 -24242
- 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
|
-
|
|
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:
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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.
|