one-code-extension 0.1.4 → 0.3.1
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/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -28,15 +28,59 @@ export interface CycleOptions {
|
|
|
28
28
|
autoInCycle: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Strip a trailing release-date stamp (`-20250601`), noise in a model id. */
|
|
32
|
+
function stripDateSuffix(id: string): string {
|
|
33
|
+
return id.replace(/-20\d{6}$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
|
-
* Shorten a model id for
|
|
33
|
-
*
|
|
34
|
-
*
|
|
37
|
+
* Shorten a model id for a compact slot. Drops an OpenRouter style `vendor/`
|
|
38
|
+
* prefix and a trailing date stamp, both of which are noise once the family name
|
|
39
|
+
* is visible: `anthropic/claude-haiku-4-5-20251001` → `haiku-4-5`.
|
|
35
40
|
*/
|
|
36
41
|
export function shortModelName(id: string): string {
|
|
37
42
|
const afterSlash = id.slice(id.lastIndexOf("/") + 1);
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
return stripDateSuffix(afterSlash).replace(/^(claude|gpt|gemini|llama|grok|mistral|deepseek|qwen)-/, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Model as `(provider) id`, e.g. `(openrouter) google/gemini-3.7-flash`. One
|
|
48
|
+
* Code runs on any provider, so the provider and the full vendor-qualified id
|
|
49
|
+
* both matter — a bare short name hides which model an aggregator route points
|
|
50
|
+
* at. Only the noisy trailing date (`-20250601`) is trimmed. Used by the footer
|
|
51
|
+
* and the startup banner so both name models the same way.
|
|
52
|
+
*/
|
|
53
|
+
export function formatModel(provider: string, id: string): string {
|
|
54
|
+
const undated = stripDateSuffix(id);
|
|
55
|
+
return provider ? `(${provider}) ${undated}` : undated;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Same as `formatModel` for a combined `provider/id` spec (what the subagent
|
|
60
|
+
* status carries): the first path segment is the provider, the rest is the id
|
|
61
|
+
* (which may itself contain slashes, e.g. `openrouter/google/gemini-3.7-flash`).
|
|
62
|
+
*/
|
|
63
|
+
export function formatModelSpec(spec: string): string {
|
|
64
|
+
const slash = spec.indexOf("/");
|
|
65
|
+
if (slash < 0) return formatModel("", spec);
|
|
66
|
+
return formatModel(spec.slice(0, slash), spec.slice(slash + 1));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* pi's placeholder model id when no provider is configured yet. A fresh machine
|
|
71
|
+
* with no key reports `{ provider: "unknown", id: "unknown" }`, which
|
|
72
|
+
* `formatModel` would render as the meaningless `(unknown) unknown`
|
|
73
|
+
* (distribution review 2026-09-09, M3). The banner and footer show "none"
|
|
74
|
+
* instead, so the first screen reads as "no provider" rather than a broken model.
|
|
75
|
+
*/
|
|
76
|
+
export const PLACEHOLDER_MODEL = "unknown";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* True when `id` names a real selected model, not pi's no-provider placeholder.
|
|
80
|
+
* A type predicate so callers narrow the id to `string` in the true branch.
|
|
81
|
+
*/
|
|
82
|
+
export function isRealModel(id: string | undefined): id is string {
|
|
83
|
+
return !!id && id !== PLACEHOLDER_MODEL;
|
|
40
84
|
}
|
|
41
85
|
|
|
42
86
|
/**
|
|
@@ -54,9 +98,13 @@ export const CYCLE_KEY_HINT = "ctrl+q to cycle";
|
|
|
54
98
|
* the way Claude Code's footer does, so the shortcut sits next to the state it
|
|
55
99
|
* changes.
|
|
56
100
|
*/
|
|
57
|
-
export function modeBadge(
|
|
101
|
+
export function modeBadge(
|
|
102
|
+
mode: PermissionMode,
|
|
103
|
+
opts?: { paused?: boolean; classifierModel?: string; streaming?: boolean },
|
|
104
|
+
): string {
|
|
58
105
|
const base = badgeBase(mode, opts);
|
|
59
|
-
|
|
106
|
+
// Claude Code appends the interrupt hint to this line while the model works.
|
|
107
|
+
return `${base} (${CYCLE_KEY_HINT})${opts?.streaming ? " · esc to interrupt" : ""}`;
|
|
60
108
|
}
|
|
61
109
|
|
|
62
110
|
function badgeBase(mode: PermissionMode, opts?: { paused?: boolean; classifierModel?: string }): string {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent for a repository's own allow rules. `.claude/settings.json` and
|
|
3
|
+
* `.claude/settings.local.json` travel with the checkout, so an allow rule in
|
|
4
|
+
* them is a grant made by whoever committed the file, not by the user running
|
|
5
|
+
* One Code — and in auto mode a rule beats the classifier, so a cloned repo
|
|
6
|
+
* shipping `Bash(curl:*)` would exfiltrate unclassified. Project hooks and
|
|
7
|
+
* project `.mcp.json` servers already need a once-per-config consent
|
|
8
|
+
* (hooks/trust.ts, mcp/trust.ts); this is the same shape for allow rules:
|
|
9
|
+
* approval keyed to a hash of the rule list, persisted under `~/.onecode`, so
|
|
10
|
+
* any change to the rules re-prompts. Deny and ask rules from the same files
|
|
11
|
+
* never prompt — they can only tighten the gate.
|
|
12
|
+
*
|
|
13
|
+
* The prompt is lazy: it appears the first time a project rule would be the
|
|
14
|
+
* deciding allow, not at startup (most sessions never hit one). A declined
|
|
15
|
+
* prompt sticks for the process, not on disk. Pure fs + hashing here; the
|
|
16
|
+
* permissions extension owns the dialog.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { oneCodeStateDir } from "../lib/paths.ts";
|
|
23
|
+
|
|
24
|
+
interface ApprovalStore {
|
|
25
|
+
version: 1;
|
|
26
|
+
approvals: Record<string, { rulesHash: string; approvedAt: string }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function projectAllowStorePath(): string {
|
|
30
|
+
return join(oneCodeStateDir(), "permissions", "project-allow-approvals.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Order-insensitive: the same rules in another order are one consent. */
|
|
34
|
+
export function hashProjectAllow(rules: readonly string[]): string {
|
|
35
|
+
return createHash("sha256").update(JSON.stringify([...rules].sort())).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readStoredProjectAllowApproval(projectRoot: string, storePath = projectAllowStorePath()): string | undefined {
|
|
39
|
+
try {
|
|
40
|
+
const store = JSON.parse(readFileSync(storePath, "utf-8")) as ApprovalStore;
|
|
41
|
+
return store.approvals?.[projectRoot]?.rulesHash;
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when the stored consent covers exactly this rule list. */
|
|
48
|
+
export function projectAllowApproved(projectRoot: string, rules: readonly string[], storePath = projectAllowStorePath()): boolean {
|
|
49
|
+
return rules.length > 0 && readStoredProjectAllowApproval(projectRoot, storePath) === hashProjectAllow(rules);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function persistProjectAllowApproval(projectRoot: string, rules: readonly string[], storePath = projectAllowStorePath()): void {
|
|
53
|
+
let store: ApprovalStore = { version: 1, approvals: {} };
|
|
54
|
+
try {
|
|
55
|
+
const existing = JSON.parse(readFileSync(storePath, "utf-8")) as ApprovalStore;
|
|
56
|
+
if (existing && typeof existing === "object" && existing.approvals) store = existing;
|
|
57
|
+
} catch {
|
|
58
|
+
// Fresh store.
|
|
59
|
+
}
|
|
60
|
+
store.approvals[projectRoot] = { rulesHash: hashProjectAllow(rules), approvedAt: new Date().toISOString() };
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(dirname(storePath), { recursive: true });
|
|
63
|
+
writeFileSync(storePath, `${JSON.stringify(store, null, "\t")}\n`);
|
|
64
|
+
} catch {
|
|
65
|
+
// Approval still holds for this session; it will just re-prompt next run.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The consent dialog's text: what the repository wants pre-approved, and which rule fired now. */
|
|
70
|
+
export function describeProjectAllow(rules: readonly string[], firing: string): { title: string; message: string } {
|
|
71
|
+
const shown = rules.slice(0, 8);
|
|
72
|
+
const more = rules.length > shown.length ? `\n… and ${rules.length - shown.length} more` : "";
|
|
73
|
+
return {
|
|
74
|
+
title: "Trust this repository's allow rules?",
|
|
75
|
+
message:
|
|
76
|
+
`This repository's .claude settings pre-approve ${rules.length} permission rule(s):\n${shown.join("\n")}${more}\n\n` +
|
|
77
|
+
`The current call matches "${firing}" and would run without a prompt` +
|
|
78
|
+
" (in auto mode, without the classifier). Whoever committed the file granted this, not you." +
|
|
79
|
+
" Approval is remembered until the rules change; declining keeps them off for this session.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -29,15 +29,22 @@ const PROTECTED_DIRS = [
|
|
|
29
29
|
// One Code's own state dir — the deny-direction twin of protecting `.claude`:
|
|
30
30
|
// as generated state moves here, a write that reconfigures the gate must
|
|
31
31
|
// stay just as guarded in its new home.
|
|
32
|
-
".
|
|
32
|
+
".onecode",
|
|
33
|
+
// Run-at-login and on-PATH locations (PERMISSIONS-REVIEW-2026-09-05 M7): a
|
|
34
|
+
// launchd agent plist runs at the next login, a fish config at the next
|
|
35
|
+
// shell, and `~/.local/bin` is where the `pi` shim and user tools live.
|
|
36
|
+
// Lowercase — segments are compared case-folded.
|
|
37
|
+
"library/launchagents",
|
|
38
|
+
".config/fish",
|
|
39
|
+
".local/bin",
|
|
33
40
|
];
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
43
|
* `.claude/worktrees` is where the agent keeps its own git worktrees, and
|
|
37
|
-
* `.
|
|
44
|
+
* `.onecode/plans` holds plan-mode documents (rendered to the user, never
|
|
38
45
|
* executed) — ordinary working space rather than configuration.
|
|
39
46
|
*/
|
|
40
|
-
const PROTECTED_DIR_EXCEPTIONS = [".claude/worktrees", ".
|
|
47
|
+
const PROTECTED_DIR_EXCEPTIONS = [".claude/worktrees", ".onecode/plans"];
|
|
41
48
|
|
|
42
49
|
const PROTECTED_FILES = new Set([
|
|
43
50
|
".gitconfig",
|
|
@@ -77,6 +84,10 @@ const PROTECTED_FILES = new Set([
|
|
|
77
84
|
"pyrightconfig.json",
|
|
78
85
|
".mcp.json",
|
|
79
86
|
".claude.json",
|
|
87
|
+
// Claude Code's managed (organisation) settings: the highest-authority
|
|
88
|
+
// permission and auto-mode configuration. Protected in every mode, not only
|
|
89
|
+
// where the auto-mode safety floor covers it (review P9).
|
|
90
|
+
"managed-settings.json",
|
|
80
91
|
]);
|
|
81
92
|
|
|
82
93
|
// `isWritingTool` / `WRITING_TOOLS` live in auto-mode/paths.ts (the shared lower
|
|
@@ -88,6 +99,8 @@ export { isWritingTool } from "../auto-mode/paths.ts";
|
|
|
88
99
|
* Whether a path is protected. Matched on path *segments* so the check works
|
|
89
100
|
* for absolute paths, cwd-relative ones, and any depth of nesting — a
|
|
90
101
|
* `packages/app/.vscode/settings.json` is as protected as a top-level one.
|
|
102
|
+
* pi's own agent directory is protected at runtime, not here — its location
|
|
103
|
+
* is `getAgentDir()`'s to know (`DecideInput.protectedDirs`).
|
|
91
104
|
*/
|
|
92
105
|
export function isProtectedPath(candidate: string, cwd?: string): boolean {
|
|
93
106
|
const forward = candidate.replace(/\\/g, "/").toLowerCase();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's own deny rules, rendered for the approval classifier (pure).
|
|
3
|
+
*
|
|
4
|
+
* A permission rule binds the spelling it names and nothing else. `Bash(rm:*)`
|
|
5
|
+
* refuses `rm scripts/build.sh` and has no opinion about
|
|
6
|
+
* `python3 -c "import os; os.remove('scripts/build.sh')"`, which is how a
|
|
7
|
+
* tiny-tier model deleted the file anyway, 3 times out of 3
|
|
8
|
+
* (WEAK-MODEL-REVIEW-2026-09-06 H2). The rule cannot be widened
|
|
9
|
+
* deterministically — an earlier attempt remembered the denied path and refused
|
|
10
|
+
* every later call naming it, which caught the retry but also refused a
|
|
11
|
+
* harmless later edit of the same file for the rest of the session, and never
|
|
12
|
+
* caught a FIRST attempt (nothing has been denied yet, so there is nothing to
|
|
13
|
+
* remember).
|
|
14
|
+
*
|
|
15
|
+
* Only something that judges by effect can generalize a rule, so the rules go
|
|
16
|
+
* to the classifier. This is one-directional, like every other model layer
|
|
17
|
+
* here: the rule still fires deterministically on its literal form before any
|
|
18
|
+
* model is consulted, so the classifier can only add blocks the spelling missed
|
|
19
|
+
* and can never clear something a rule already refused.
|
|
20
|
+
*
|
|
21
|
+
* The lines are appended at Claude Code's own HARD BLOCK injection point
|
|
22
|
+
* (`classifier-prompt.ts buildRuleset`), which is the sanctioned append-only
|
|
23
|
+
* surface, so prompt fidelity is untouched. They are indexed by the same scan
|
|
24
|
+
* as the built-in rules (`rules.ts`), so a verdict citing one grounds.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { parseRule } from "./matcher.ts";
|
|
28
|
+
|
|
29
|
+
/** Bound on how many rules are described, so a long deny list cannot dominate the ruleset. */
|
|
30
|
+
const MAX_RULES = 25;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bound on the quoted rule spec, so a pathological pattern cannot dominate
|
|
34
|
+
* either. The bound is on the SPEC and not on the finished line: clipping the
|
|
35
|
+
* line would cut the instruction off its own rule.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_SPEC = 120;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A rule name must survive `ruleNameFromLine`, which cuts at the first `:` or
|
|
41
|
+
* ` [`. Rule specs contain both (`Bash(rm:*)`, `Read(~/**)`), so the name
|
|
42
|
+
* carries a punctuation-stripped form and the verbatim spec goes in the body.
|
|
43
|
+
*/
|
|
44
|
+
function nameFragment(spec: string): string {
|
|
45
|
+
// The rule's own spelling, not the normalized tool name: this name is what the
|
|
46
|
+
// user is shown when a verdict cites the rule, so `WebFetch` should not come
|
|
47
|
+
// back as `web_fetch`.
|
|
48
|
+
const cleaned = spec
|
|
49
|
+
.replace(/[:[\]`*()]/g, " ")
|
|
50
|
+
.replace(/\s+/g, " ")
|
|
51
|
+
.trim();
|
|
52
|
+
return cleaned.slice(0, 60).trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One HARD BLOCK line per deny rule. `ask` rules are deliberately NOT rendered:
|
|
57
|
+
* an ask rule asks for a decision, and auto mode already routes those to the
|
|
58
|
+
* classifier — turning them into block rules would convert "check with me" into
|
|
59
|
+
* "never", which is not what the user wrote.
|
|
60
|
+
*/
|
|
61
|
+
export function denyRuleLines(deny: readonly string[]): string[] {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
for (const raw of deny) {
|
|
65
|
+
if (lines.length >= MAX_RULES) break;
|
|
66
|
+
const rule = parseRule(raw);
|
|
67
|
+
if (!rule) continue;
|
|
68
|
+
const full = rule.raw.replace(/[\n\r]+/g, " ").trim();
|
|
69
|
+
if (!full || seen.has(full)) continue;
|
|
70
|
+
seen.add(full);
|
|
71
|
+
const spec = full.length > MAX_SPEC ? `${full.slice(0, MAX_SPEC - 1)}…` : full;
|
|
72
|
+
lines.push(
|
|
73
|
+
`User Deny Rule ${nameFragment(spec)}: the user's own settings deny \`${spec}\`. ` +
|
|
74
|
+
"That is the user's standing decision about a CLASS of action, made in advance, so user intent does NOT clear it. " +
|
|
75
|
+
"BLOCK the action under review if it would achieve what that rule forbids, whatever command, interpreter, script or tool it is spelled with.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What "Yes, and don't ask again this session" grants (pure).
|
|
3
|
+
*
|
|
4
|
+
* Until 2026-09-05 the option minted a bare tool rule for every tool but bash:
|
|
5
|
+
* one approved in-project write granted every write to every path for the rest
|
|
6
|
+
* of the process, one fetch every URL, one MCP call every argument, and in auto
|
|
7
|
+
* mode the minted rule stood as a classifier bypass for that tool
|
|
8
|
+
* (PERMISSIONS-REVIEW-2026-09-05 M2). Claude Code's equivalents are scoped —
|
|
9
|
+
* "allow all edits during this session" is working-directory-scoped, WebFetch
|
|
10
|
+
* grants a domain — and the option names what it grants. So does this: the
|
|
11
|
+
* grant is scoped by the subject's kind (permissions/matcher.ts `subjectKind`),
|
|
12
|
+
* the label says exactly what it covers, and there is no grant at all where a
|
|
13
|
+
* rule could not matter (a protected path, the auto-mode safety floor — both are
|
|
14
|
+
* checked before allow rules) or where the classifier is the boundary (auto
|
|
15
|
+
* mode: session allows are never applied there).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import { toAbsolute } from "../auto-mode/paths.ts";
|
|
20
|
+
import { escapeLiteral, isAtOrInsideDir, normalizeToolName, parseRule, type PermissionMode, type PermissionRule, subjectKind, urlHost } from "./matcher.ts";
|
|
21
|
+
|
|
22
|
+
export interface SessionGrant {
|
|
23
|
+
/** The rule to add to the session's allow list when the user picks this option. */
|
|
24
|
+
rule: PermissionRule;
|
|
25
|
+
/** The option label, naming the scope granted. */
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionGrantInput {
|
|
30
|
+
toolName: string;
|
|
31
|
+
/** The subject the prompt shows (the original command in a worktree session). */
|
|
32
|
+
subject: string;
|
|
33
|
+
/** The directory the call runs in (a worktree's path in a worktree session). */
|
|
34
|
+
cwd: string;
|
|
35
|
+
mode: PermissionMode;
|
|
36
|
+
/** The `decide()` cause behind the prompt. */
|
|
37
|
+
cause: string;
|
|
38
|
+
/** The auto-mode safety floor raised this prompt — no rule can cover it. */
|
|
39
|
+
floor?: boolean;
|
|
40
|
+
home: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `~`-abbreviated path for a label. */
|
|
44
|
+
function displayPath(path: string, home: string): string {
|
|
45
|
+
return path === home ? "~" : path.startsWith(`${home}/`) ? `~/${path.slice(home.length + 1)}` : path;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Claude Code's `//absolute` rule form for a directory and everything under it. */
|
|
49
|
+
function absoluteDirPattern(dir: string): string {
|
|
50
|
+
return `/${dir.replace(/\/+$/, "")}/**`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sessionGrant(input: SessionGrantInput): SessionGrant | undefined {
|
|
54
|
+
const { subject, cwd, mode, cause, home } = input;
|
|
55
|
+
// Auto mode never applies session allows (the classifier is the boundary and
|
|
56
|
+
// its prompts are resume/floor prompts, not per-action approvals); protected
|
|
57
|
+
// paths and the safety floor are judged before allow rules, so a rule minted
|
|
58
|
+
// here could never cover them.
|
|
59
|
+
if (mode === "auto" || input.floor || cause === "protected-path") return undefined;
|
|
60
|
+
|
|
61
|
+
const tool = normalizeToolName(input.toolName);
|
|
62
|
+
const mint = (raw: string, label: string): SessionGrant | undefined => {
|
|
63
|
+
const rule = parseRule(raw);
|
|
64
|
+
return rule ? { rule, label } : undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
switch (subjectKind(tool)) {
|
|
68
|
+
case "command":
|
|
69
|
+
// The approved command, as an exact literal — never a glob.
|
|
70
|
+
if (!subject) return undefined;
|
|
71
|
+
return mint(`${tool}(${escapeLiteral(subject)})`, "Yes, and don't ask again for this exact command this session");
|
|
72
|
+
case "path": {
|
|
73
|
+
if (!subject) return undefined;
|
|
74
|
+
const absolute = toAbsolute(cwd, subject, home);
|
|
75
|
+
// Inside the working directory this is Claude Code's "allow all edits
|
|
76
|
+
// during this session"; outside, the grant stops at the file's directory.
|
|
77
|
+
if (isAtOrInsideDir(absolute, cwd, cwd)) {
|
|
78
|
+
return mint(`${tool}(${absoluteDirPattern(cwd)})`, `Yes, and allow ${tool} anywhere in the working directory this session`);
|
|
79
|
+
}
|
|
80
|
+
const dir = dirname(absolute);
|
|
81
|
+
return mint(`${tool}(${absoluteDirPattern(dir)})`, `Yes, and allow ${tool} under ${displayPath(dir, home)} this session`);
|
|
82
|
+
}
|
|
83
|
+
case "url": {
|
|
84
|
+
const host = urlHost(subject);
|
|
85
|
+
if (!host) return undefined;
|
|
86
|
+
return mint(`${tool}(domain:${host})`, `Yes, and don't ask again for ${host} this session`);
|
|
87
|
+
}
|
|
88
|
+
case "text":
|
|
89
|
+
return mint(tool, `Yes, and don't ask again for ${tool} this session`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -2,23 +2,55 @@
|
|
|
2
2
|
* Claude Code settings.json permissions loading/merging/persistence (pure fs).
|
|
3
3
|
*
|
|
4
4
|
* Sources, lowest to highest precedence:
|
|
5
|
-
* ~/.claude/settings.json
|
|
6
|
-
*
|
|
7
|
-
* <cwd>/.claude/settings.
|
|
5
|
+
* ~/.claude/settings.json (user, borrowed — read only)
|
|
6
|
+
* ~/.onecode/settings.json (One Code global — read + write)
|
|
7
|
+
* <cwd>/.claude/settings.json (project, checked in — read only)
|
|
8
|
+
* <cwd>/.claude/settings.local.json (project, personal — read only)
|
|
9
|
+
* ~/.onecode/projects/<slug>/settings.json (One Code per repo — read + write)
|
|
10
|
+
* <managed-settings.json> (organisation policy — read only, highest)
|
|
8
11
|
*
|
|
9
|
-
* allow/deny/ask arrays concatenate across sources
|
|
10
|
-
*
|
|
12
|
+
* allow/deny/ask arrays concatenate across sources — except that the two
|
|
13
|
+
* in-repo files' allow rules land in `projectAllow`, gated behind consent
|
|
14
|
+
* (review P10); defaultMode from the most specific `.claude` source wins, with
|
|
15
|
+
* managed settings above all (the One Code files contribute rules only).
|
|
16
|
+
* One Code persists the rules it records to its own files, never into Claude
|
|
17
|
+
* Code's — `persistAllowRule` is pointed at a One Code path by its caller.
|
|
18
|
+
* Unknown keys in the files are preserved on write.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
21
|
+
import { managedSettingsPaths } from "../auto-mode/config.ts";
|
|
22
|
+
import { readSettingsFile as readClaudeSettingsFile, settingsPaths } from "../lib/claude-settings.ts";
|
|
23
|
+
import {
|
|
24
|
+
oneCodeProjectSettingsPath,
|
|
25
|
+
oneCodeSettingsPath,
|
|
26
|
+
readSettingsForWrite,
|
|
27
|
+
writeSettings,
|
|
28
|
+
} from "../lib/one-code-settings.ts";
|
|
15
29
|
import type { PermissionMode } from "./matcher.ts";
|
|
16
30
|
|
|
31
|
+
export { settingsPaths };
|
|
32
|
+
|
|
17
33
|
export interface PermissionSettings {
|
|
34
|
+
/** Allow rules from sources the user wrote or that outrank the repo (user, One Code, managed). */
|
|
18
35
|
allow: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Allow rules from the repository's own `.claude/settings.json` /
|
|
38
|
+
* `settings.local.json`. A checked-in file pre-approving `Bash(curl:*)` is a
|
|
39
|
+
* grant the user never made, so these apply only after a once-per-config
|
|
40
|
+
* consent (project-trust.ts) — deny and ask rules from the same files load
|
|
41
|
+
* freely, since they can only tighten the gate.
|
|
42
|
+
*/
|
|
43
|
+
projectAllow: string[];
|
|
19
44
|
deny: string[];
|
|
20
45
|
ask: string[];
|
|
21
46
|
defaultMode?: PermissionMode;
|
|
47
|
+
/**
|
|
48
|
+
* Claude Code's `permissions.disableBypassPermissionsMode: "disable"` seen in
|
|
49
|
+
* any source: bypassPermissions is refused however it is requested (flag,
|
|
50
|
+
* `--dangerously-skip-permissions`, settings). A restriction, so every scope
|
|
51
|
+
* may set it — the repo included.
|
|
52
|
+
*/
|
|
53
|
+
disableBypassPermissionsMode?: boolean;
|
|
22
54
|
}
|
|
23
55
|
|
|
24
56
|
interface ClaudeSettingsFile {
|
|
@@ -27,10 +59,26 @@ interface ClaudeSettingsFile {
|
|
|
27
59
|
deny?: string[];
|
|
28
60
|
ask?: string[];
|
|
29
61
|
defaultMode?: string;
|
|
62
|
+
disableBypassPermissionsMode?: string;
|
|
30
63
|
};
|
|
31
64
|
[key: string]: unknown;
|
|
32
65
|
}
|
|
33
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Modes a repository's own files (`<cwd>/.claude/settings.json`,
|
|
69
|
+
* `settings.local.json`) may NOT select — honoured from user and managed scope
|
|
70
|
+
* only. Both files live in the checkout, so honouring these there lets a cloned
|
|
71
|
+
* repo grant itself the mode: `auto`, whose classifier is what contains it
|
|
72
|
+
* (Claude Code makes the same exclusion), and `bypassPermissions`, which has no
|
|
73
|
+
* classifier, no prompt and no protected path at all — the footer badge is the
|
|
74
|
+
* only sign (PERMISSIONS-REVIEW-2026-09-05 H3, measured: a checked-in
|
|
75
|
+
* `bypassPermissions` ran an outside-project write with no prompt). `dontAsk`
|
|
76
|
+
* and `acceptEdits` stay honoured: neither can approve what default mode would
|
|
77
|
+
* not (dontAsk turns asks into denials; acceptEdits is confined to the working
|
|
78
|
+
* directory).
|
|
79
|
+
*/
|
|
80
|
+
export const MODES_NEVER_FROM_PROJECT: ReadonlySet<PermissionMode> = new Set<PermissionMode>(["auto", "bypassPermissions"]);
|
|
81
|
+
|
|
34
82
|
const MODES: PermissionMode[] = ["default", "acceptEdits", "plan", "bypassPermissions", "dontAsk", "auto"];
|
|
35
83
|
|
|
36
84
|
/** Display-name aliases Claude Code accepts wherever a mode is named. */
|
|
@@ -47,39 +95,41 @@ export function normalizePermissionMode(value: unknown): PermissionMode | undefi
|
|
|
47
95
|
}
|
|
48
96
|
|
|
49
97
|
function readSettingsFile(path: string): ClaudeSettingsFile | undefined {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
} catch {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function settingsPaths(cwd: string, home: string): { user: string; project: string; local: string } {
|
|
59
|
-
return {
|
|
60
|
-
user: join(home, ".claude", "settings.json"),
|
|
61
|
-
project: join(cwd, ".claude", "settings.json"),
|
|
62
|
-
local: join(cwd, ".claude", "settings.local.json"),
|
|
63
|
-
};
|
|
98
|
+
// Shared reader (lib/claude-settings.ts) — this module narrows to the
|
|
99
|
+
// permissions-relevant shape.
|
|
100
|
+
return readClaudeSettingsFile(path) as ClaudeSettingsFile | undefined;
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
export function loadPermissionSettings(cwd: string, home: string): PermissionSettings {
|
|
67
104
|
const paths = settingsPaths(cwd, home);
|
|
68
|
-
const merged: PermissionSettings = { allow: [], deny: [], ask: [] };
|
|
105
|
+
const merged: PermissionSettings = { allow: [], projectAllow: [], deny: [], ask: [] };
|
|
106
|
+
|
|
107
|
+
// One Code writes the rules it records (via /allow, the auto-mode setup) to its
|
|
108
|
+
// own files, never into Claude Code's — so its own files are read alongside the
|
|
109
|
+
// borrowed `.claude` ladder. They contribute allow/deny/ask rules only;
|
|
110
|
+
// `defaultMode` stays sourced from the `.claude` files (nothing here writes it,
|
|
111
|
+
// and keeping the auto-mode reasoning to one place avoids a second `auto` path).
|
|
112
|
+
const oneCodeGlobal = oneCodeSettingsPath(home);
|
|
113
|
+
const oneCodeProject = oneCodeProjectSettingsPath(cwd, home);
|
|
69
114
|
|
|
70
|
-
|
|
115
|
+
// Managed settings last: Claude Code's organisation policy outranks every
|
|
116
|
+
// user/project file, for rules and for defaultMode alike (review P14).
|
|
117
|
+
const managed = managedSettingsPaths();
|
|
118
|
+
for (const path of [paths.user, oneCodeGlobal, paths.project, paths.local, oneCodeProject, ...managed]) {
|
|
71
119
|
const file = readSettingsFile(path);
|
|
72
120
|
const perms = file?.permissions;
|
|
73
121
|
if (!perms) continue;
|
|
74
|
-
|
|
122
|
+
const allowTarget = path === paths.project || path === paths.local ? merged.projectAllow : merged.allow;
|
|
123
|
+
if (Array.isArray(perms.allow)) allowTarget.push(...perms.allow.filter((r) => typeof r === "string"));
|
|
75
124
|
if (Array.isArray(perms.deny)) merged.deny.push(...perms.deny.filter((r) => typeof r === "string"));
|
|
76
125
|
if (Array.isArray(perms.ask)) merged.ask.push(...perms.ask.filter((r) => typeof r === "string"));
|
|
126
|
+
if (perms.disableBypassPermissionsMode === "disable") merged.disableBypassPermissionsMode = true;
|
|
127
|
+
if (path === oneCodeGlobal || path === oneCodeProject) continue;
|
|
77
128
|
const defaultMode = normalizePermissionMode(perms.defaultMode);
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (defaultMode && !(defaultMode === "auto" && path !== paths.user)) {
|
|
129
|
+
// The modes a repo may not grant itself (MODES_NEVER_FROM_PROJECT) are
|
|
130
|
+
// honoured from user and managed scope only.
|
|
131
|
+
const fromProject = path === paths.project || path === paths.local;
|
|
132
|
+
if (defaultMode && !(fromProject && MODES_NEVER_FROM_PROJECT.has(defaultMode))) {
|
|
83
133
|
merged.defaultMode = defaultMode;
|
|
84
134
|
}
|
|
85
135
|
}
|
|
@@ -87,12 +137,41 @@ export function loadPermissionSettings(cwd: string, home: string): PermissionSet
|
|
|
87
137
|
return merged;
|
|
88
138
|
}
|
|
89
139
|
|
|
90
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* The mode a session starts in, from the modes requested in Claude Code's
|
|
142
|
+
* precedence order (`--dangerously-skip-permissions`, `--permission-mode`,
|
|
143
|
+
* settings' `defaultMode`), skipping `bypassPermissions` wherever a settings
|
|
144
|
+
* source disabled it (CC's `permissionSetup.ts`). `mode` is undefined when
|
|
145
|
+
* nothing usable was requested — the caller keeps its live mode, so a
|
|
146
|
+
* mid-session reload never undoes a ctrl+q switch. `bypassRefused` says a
|
|
147
|
+
* bypass request was skipped, for CC's "disabled by settings" notification.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveStartupMode(
|
|
150
|
+
requested: (PermissionMode | undefined)[],
|
|
151
|
+
settings: Pick<PermissionSettings, "disableBypassPermissionsMode">,
|
|
152
|
+
): { mode?: PermissionMode; bypassRefused: boolean } {
|
|
153
|
+
let bypassRefused = false;
|
|
154
|
+
for (const candidate of requested) {
|
|
155
|
+
if (!candidate) continue;
|
|
156
|
+
if (candidate === "bypassPermissions" && settings.disableBypassPermissionsMode) {
|
|
157
|
+
bypassRefused = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
return { mode: candidate, bypassRefused };
|
|
161
|
+
}
|
|
162
|
+
return { bypassRefused };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Append an allow rule to a One Code settings file, creating it if needed.
|
|
167
|
+
* Strict read + atomic write, like the other `~/.onecode` writers: a malformed
|
|
168
|
+
* file is not silently clobbered (it may also hold classifierModel/subagentModel),
|
|
169
|
+
* and a half-written file is never visible to a concurrent reader.
|
|
170
|
+
*/
|
|
91
171
|
export function persistAllowRule(rule: string, filePath: string): void {
|
|
92
|
-
const file =
|
|
172
|
+
const file = readSettingsForWrite(filePath) as ClaudeSettingsFile;
|
|
93
173
|
const permissions = (file.permissions ??= {});
|
|
94
174
|
const allow = (permissions.allow ??= []);
|
|
95
175
|
if (!allow.includes(rule)) allow.push(rule);
|
|
96
|
-
|
|
97
|
-
writeFileSync(filePath, `${JSON.stringify(file, null, 2)}\n`);
|
|
176
|
+
writeSettings(filePath, file as Record<string, unknown>);
|
|
98
177
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parent→child permission bridge.
|
|
3
|
+
*
|
|
4
|
+
* In-process subagent sessions get a `noExtensions` loader, so the real
|
|
5
|
+
* `permissions` extension never loads inside them. To match Claude Code — where a
|
|
6
|
+
* subagent's tool calls go through the *same* permission pipeline as the main
|
|
7
|
+
* agent (mode inheritance, the auto-mode classifier, and interactive prompts that
|
|
8
|
+
* bubble to the user; findings §17.1) — the parent's `permissions` extension
|
|
9
|
+
* publishes a decision closure on this channel. `subagents` captures it and
|
|
10
|
+
* threads it into the child runner, whose `permission-gate` calls it for every
|
|
11
|
+
* child tool call instead of its fail-closed local fallback.
|
|
12
|
+
*
|
|
13
|
+
* The closure is invoked in-process from the child's `tool_call` handler; it runs
|
|
14
|
+
* against the parent's live mode/rules/ctx (so a prompt renders on the parent's
|
|
15
|
+
* terminal). A plain function over `pi.events` is the only transport that works:
|
|
16
|
+
* each child session has its own EventBus, so `pi.events` does not cross the
|
|
17
|
+
* boundary — but the parent's `subagents` and `permissions` extensions share one,
|
|
18
|
+
* and the closure is carried the rest of the way as an ordinary reference (the
|
|
19
|
+
* same shape as `MCP_TOOLS_CHANNEL` sharing tool definitions).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { type BridgeWatchApi, watchBridge } from "../lib/bridge-watch.ts";
|
|
23
|
+
|
|
24
|
+
export const SUBAGENT_GATE_CHANNEL = "one-code:subagent-permission-gate";
|
|
25
|
+
|
|
26
|
+
/** One child tool call to evaluate. `cwd` is the child's runtime cwd (a worktree, if isolated). */
|
|
27
|
+
export interface ChildToolCall {
|
|
28
|
+
toolName: string;
|
|
29
|
+
input: Record<string, unknown>;
|
|
30
|
+
cwd: string;
|
|
31
|
+
/**
|
|
32
|
+
* The child's live turn signal: a classifier call is aborted when the child
|
|
33
|
+
* turn is, and a prompt bubbled to the user is dismissed (as a denial) when
|
|
34
|
+
* the child is stopped mid-prompt — otherwise a dead agent's dialog stays on
|
|
35
|
+
* screen and holds the prompt chain (SUBAGENT-REVIEW M5).
|
|
36
|
+
*/
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
/** The child session's id (what the gate can see); the runner maps it to `agent`. */
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
/** The run name of the asking agent, for the prompt title — set by the runner, not the gate. */
|
|
41
|
+
agent?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A permission decision: `undefined` = allow; otherwise block with a model-facing reason. */
|
|
45
|
+
export type ChildGateDecision = { block: true; reason: string } | undefined;
|
|
46
|
+
|
|
47
|
+
/** The parent's decision closure, threaded into a child's permission gate. */
|
|
48
|
+
export type PermissionBridge = (call: ChildToolCall) => Promise<ChildGateDecision>;
|
|
49
|
+
|
|
50
|
+
export interface SubagentGatePayload {
|
|
51
|
+
decide: PermissionBridge;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A lazy getter for the latest published permission bridge (`lib/bridge-watch.ts`); undefined until the permissions extension publishes. */
|
|
55
|
+
export function watchPermissionBridge(pi: BridgeWatchApi): () => PermissionBridge | undefined {
|
|
56
|
+
return watchBridge(pi, SUBAGENT_GATE_CHANNEL, (data) => (data as SubagentGatePayload | undefined)?.decide);
|
|
57
|
+
}
|