one-code-extension 0.1.3 → 0.3.0
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 +200 -172
- 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 +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -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 -202
- 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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission gate for in-process agent sessions (pure), shared by the workflow
|
|
3
|
+
* and subagent runners.
|
|
4
|
+
*
|
|
5
|
+
* In-process agent sessions are built with `noExtensions: true`, so One Code's
|
|
6
|
+
* own permissions extension never loads inside them — without this gate every
|
|
7
|
+
* subagent bash/edit/write would run unchecked. `DefaultResourceLoader` always
|
|
8
|
+
* loads explicitly passed `extensionFactories` (even under noExtensions),
|
|
9
|
+
* which is where this inline factory attaches.
|
|
10
|
+
*
|
|
11
|
+
* Primary path: every tool call is routed through the parent's permission
|
|
12
|
+
* BRIDGE (`permissions/subagent-gate.ts`) — the parent's live mode, rules,
|
|
13
|
+
* auto-mode classifier and interactive prompts, exactly as the main agent's
|
|
14
|
+
* calls are judged (Claude Code parity, findings §17.1). Resolving or invoking
|
|
15
|
+
* the bridge fails CLOSED.
|
|
16
|
+
*
|
|
17
|
+
* Local fallback (no bridge — workflow/headless runs, or a parent without the
|
|
18
|
+
* permissions extension): deny rules always win; explicit allow rules allow;
|
|
19
|
+
* the mode is the parent session's live mode (published as CC_PERMISSION_MODE,
|
|
20
|
+
* read per call), else the settings' defaultMode, else acceptEdits (Claude Code
|
|
21
|
+
* runs these agents in acceptEdits); ask rules are honoured; anything that
|
|
22
|
+
* would normally *ask* is denied — there is no interactive prompt inside an
|
|
23
|
+
* in-process agent, and fail-closed beats silently trusting the model. Auto
|
|
24
|
+
* mode has no classifier here, so it degrades to acceptEdits, which decide()
|
|
25
|
+
* confines to the cwd (as it does reads); the rest is denied. Project-scope
|
|
26
|
+
* allow rules apply only with a stored consent (project-trust.ts).
|
|
27
|
+
*
|
|
28
|
+
* `neverGate` names tools the runtime itself injects (e.g. `structured_output`,
|
|
29
|
+
* the child-only `SendMessage`-to-main tool) that must never be gated.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { getAgentDir, type InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
33
|
+
import { findProjectRoot } from "./git.ts";
|
|
34
|
+
import { memoryDir } from "./memory.ts";
|
|
35
|
+
import { claudeConfigDir, oneCodeStateDir } from "./paths.ts";
|
|
36
|
+
import { sessionResultsDir } from "./persisted-output.ts";
|
|
37
|
+
import { sessionScratchpadDir } from "./scratchpad.ts";
|
|
38
|
+
import { decide, extractSubject, isPathSubjectTool, normalizeToolName, type PermissionMode, parseRules } from "../permissions/matcher.ts";
|
|
39
|
+
import { projectAllowApproved } from "../permissions/project-trust.ts";
|
|
40
|
+
import { loadPermissionSettings, normalizePermissionMode } from "../permissions/settings.ts";
|
|
41
|
+
import type { PermissionBridge } from "../permissions/subagent-gate.ts";
|
|
42
|
+
import { resolveForContainment, toAbsolute } from "../auto-mode/paths.ts";
|
|
43
|
+
|
|
44
|
+
/** A directory's realpath for containment, or the path itself when nothing about it resolves. */
|
|
45
|
+
export const resolvedOrSelf = (dir: string) => resolveForContainment(dir) ?? dir;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Directories protected at runtime, beyond protected-paths.ts's static list:
|
|
49
|
+
* pi's own agent directory (`getAgentDir()` — `~/.pi/agent` for stock pi,
|
|
50
|
+
* `~/.onecode/agent` bundled), whose extensions, settings and auth are code
|
|
51
|
+
* and credentials the harness loads; Claude Code's config dir and One Code's
|
|
52
|
+
* state dir, which hold the permission settings the gate is made of. The
|
|
53
|
+
* segment list in protected-paths.ts already catches literal `.claude` /
|
|
54
|
+
* `.onecode`; these absolute roots also catch them when `CLAUDE_CONFIG_DIR` /
|
|
55
|
+
* `ONECODE_STATE_DIR` relocated the dir (distribution review 2026-09-09, L2).
|
|
56
|
+
* The memory dir (under the config dir) and the plan file (under the state dir)
|
|
57
|
+
* are cleared as harness working space *before* the protected check in
|
|
58
|
+
* `decide()`, so protecting these roots does not block them. Literal and
|
|
59
|
+
* resolved spelling, so both the raw and the realpath'd subject match. One
|
|
60
|
+
* definition for both gates (PERMISSIONS-REVIEW-2026-09-05 M7).
|
|
61
|
+
*/
|
|
62
|
+
export function runtimeProtectedDirs(): string[] {
|
|
63
|
+
const roots = [getAgentDir(), claudeConfigDir(), oneCodeStateDir()];
|
|
64
|
+
return [...new Set(roots.flatMap((dir) => [dir, resolvedOrSelf(dir)]))];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tools the runtime itself injects; never gate them. */
|
|
68
|
+
const DEFAULT_INTERNAL_TOOLS = new Set(["structured_output"]);
|
|
69
|
+
|
|
70
|
+
/** The parent permissions extension publishes its live mode here (in-process children read it). */
|
|
71
|
+
export const MODE_ENV = "CC_PERMISSION_MODE";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The mode the local gate judges under: the parent's published live mode, else
|
|
75
|
+
* the settings' defaultMode, else Claude Code's subagent default. Pure, for tests.
|
|
76
|
+
*/
|
|
77
|
+
export function localGateMode(envValue: string | undefined, defaultMode: PermissionMode | undefined): PermissionMode {
|
|
78
|
+
return normalizePermissionMode(envValue) ?? defaultMode ?? "acceptEdits";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function permissionGateFactory(
|
|
82
|
+
cwd: string,
|
|
83
|
+
home: string,
|
|
84
|
+
neverGate: Set<string> = DEFAULT_INTERNAL_TOOLS,
|
|
85
|
+
/**
|
|
86
|
+
* The parent permissions extension's decision closure. When present, a child's
|
|
87
|
+
* tool calls are gated through the REAL parent pipeline (mode inheritance,
|
|
88
|
+
* auto-mode classifier, prompts bubbled to the user — Claude Code parity,
|
|
89
|
+
* findings §17.1) rather than the fail-closed local rules below. A getter so it
|
|
90
|
+
* can be read at call time (the bridge may be published after the loader builds);
|
|
91
|
+
* absent only for headless runs with no publishing parent, which keep the local gate.
|
|
92
|
+
*/
|
|
93
|
+
getBridge?: () => PermissionBridge | undefined,
|
|
94
|
+
): InlineExtension {
|
|
95
|
+
const settings = loadPermissionSettings(cwd, home);
|
|
96
|
+
const deny = parseRules(settings.deny);
|
|
97
|
+
const ask = parseRules(settings.ask);
|
|
98
|
+
// The repository the run belongs to: an isolation worktree shares its main
|
|
99
|
+
// checkout's consent and memory (findProjectRoot), not a throwaway slug.
|
|
100
|
+
const projectRoot = findProjectRoot(cwd) ?? cwd;
|
|
101
|
+
// Repo-shipped allow rules count only once the user has consented to exactly
|
|
102
|
+
// this list (the parent session's dialog); there is no one to ask here.
|
|
103
|
+
const allow = parseRules(
|
|
104
|
+
projectAllowApproved(projectRoot, settings.projectAllow) ? [...settings.allow, ...settings.projectAllow] : settings.allow,
|
|
105
|
+
);
|
|
106
|
+
// Memory writes work inside agent sessions too — otherwise the protected
|
|
107
|
+
// `.claude` check turns them into "needs interactive approval" and the
|
|
108
|
+
// harness blocks its own feature (same rationale as in decide()).
|
|
109
|
+
const memoryDirPath = memoryDir(home, projectRoot);
|
|
110
|
+
const protectedDirs = runtimeProtectedDirs();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name: "agent-permission-gate",
|
|
114
|
+
hidden: true,
|
|
115
|
+
factory: (pi) => {
|
|
116
|
+
// The scratchpad embeds the *child's* session id, which does not exist
|
|
117
|
+
// until the session runs — derived on first tool call, then pinned.
|
|
118
|
+
let scratchpadDirPath: string | undefined;
|
|
119
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
120
|
+
if (neverGate.has(event.toolName)) return undefined;
|
|
121
|
+
const runCwd = ctx?.cwd ?? cwd;
|
|
122
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
123
|
+
|
|
124
|
+
// Preferred path: route the call through the parent's real permission
|
|
125
|
+
// pipeline. Everything here — resolving the bridge AND invoking it — fails
|
|
126
|
+
// CLOSED (deny); a broken bridge or getter must never silently open the gate.
|
|
127
|
+
// The child's own signal rides along so an aborted child turn cancels any
|
|
128
|
+
// classifier call the bridge makes and dismisses a prompt it bubbled; the
|
|
129
|
+
// session id lets the runner name the asking agent in that prompt.
|
|
130
|
+
try {
|
|
131
|
+
const bridge = getBridge?.();
|
|
132
|
+
if (bridge) {
|
|
133
|
+
const input = (event.input ?? {}) as Record<string, unknown>;
|
|
134
|
+
return await bridge({
|
|
135
|
+
toolName: event.toolName,
|
|
136
|
+
input,
|
|
137
|
+
cwd: runCwd,
|
|
138
|
+
signal: ctx?.signal,
|
|
139
|
+
sessionId,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { block: true, reason: `Permission bridge failed (${(error as Error).message}); denied to fail safe.` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!scratchpadDirPath && sessionId) scratchpadDirPath = sessionScratchpadDir(runCwd, sessionId);
|
|
147
|
+
const tool = normalizeToolName(event.toolName);
|
|
148
|
+
const subject = extractSubject(tool, event.input as Record<string, unknown>);
|
|
149
|
+
// Path tools (writers and the read tier) are judged by where the path
|
|
150
|
+
// resolves; both sides resolved (macOS /var → /private/var) so a real
|
|
151
|
+
// in-cwd path is not misjudged as an escape.
|
|
152
|
+
const resolvedSubject =
|
|
153
|
+
isPathSubjectTool(tool) && subject ? resolveForContainment(toAbsolute(runCwd, subject, home)) : undefined;
|
|
154
|
+
// Read per call: the parent may cycle modes while a child runs.
|
|
155
|
+
const liveMode = localGateMode(process.env[MODE_ENV], settings.defaultMode);
|
|
156
|
+
// No classifier is reachable without the bridge, so auto mode is judged
|
|
157
|
+
// as acceptEdits — which decide() confines to the working directory.
|
|
158
|
+
const mode = liveMode === "auto" ? "acceptEdits" : liveMode;
|
|
159
|
+
const result = decide({
|
|
160
|
+
toolName: event.toolName,
|
|
161
|
+
subject,
|
|
162
|
+
cwd: runCwd,
|
|
163
|
+
mode,
|
|
164
|
+
deny,
|
|
165
|
+
ask,
|
|
166
|
+
allow,
|
|
167
|
+
resolvedSubject,
|
|
168
|
+
resolvedCwd: resolveForContainment(runCwd),
|
|
169
|
+
memoryDirPath,
|
|
170
|
+
scratchpadDirPath,
|
|
171
|
+
// The agent's own persisted-output dir: a read of a result it was
|
|
172
|
+
// handed a path to must not be judged an outside read. Resolved like
|
|
173
|
+
// the subject (the tmpdir fallback sits under a symlinked /var on macOS).
|
|
174
|
+
resultsDirPath: resolvedOrSelf(sessionResultsDir(ctx)),
|
|
175
|
+
protectedDirs,
|
|
176
|
+
});
|
|
177
|
+
if (result.decision === "allow") return undefined;
|
|
178
|
+
const ruleNote = result.rule ? ` (rule: ${result.rule.raw})` : "";
|
|
179
|
+
const outside = result.cause === "working-dir" ? " outside the working directory" : "";
|
|
180
|
+
return {
|
|
181
|
+
block: true,
|
|
182
|
+
reason:
|
|
183
|
+
result.decision === "deny"
|
|
184
|
+
? `Denied by permission rules${ruleNote}.`
|
|
185
|
+
: liveMode === "auto"
|
|
186
|
+
? `Auto mode's classifier is only reachable through the parent session, which this agent has no link to; a call${outside} is denied to fail safe.`
|
|
187
|
+
: `This action${outside} needs interactive approval, which is not available inside an in-process agent. Ask for it to be added to the allow rules, or work around it.`,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -13,8 +13,26 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
16
17
|
import { join } from "node:path";
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Where a session's oversized outputs are persisted — the session dir,
|
|
21
|
+
* matching Claude Code's `<session-dir>/tool-results/<id>.txt`. A session-less
|
|
22
|
+
* run (`--no-session`) has no such dir, so a temp folder serves; the model gets
|
|
23
|
+
* the path either way. Shared by every producer that would otherwise truncate:
|
|
24
|
+
* cutting text loses context, a file keeps it one `read` away.
|
|
25
|
+
*/
|
|
26
|
+
export function sessionResultsDir(ctx: { sessionManager?: { getSessionDir?: () => string | undefined } } | undefined): string {
|
|
27
|
+
try {
|
|
28
|
+
const dir = ctx?.sessionManager?.getSessionDir?.();
|
|
29
|
+
if (dir) return dir;
|
|
30
|
+
} catch {
|
|
31
|
+
// fall through to the temp folder
|
|
32
|
+
}
|
|
33
|
+
return join(tmpdir(), "one-code");
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
/** Claude Code persists around this size; pi's bash truncation uses 50KB too. */
|
|
19
37
|
export const PERSIST_MAX_BYTES = 50 * 1024;
|
|
20
38
|
export const PREVIEW_BYTES = 2 * 1024;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored token-estimation and max-tokens clamp helpers from pi-ai.
|
|
3
|
+
*
|
|
4
|
+
* The compaction extension needs `clampMaxTokensToContext` (from pi-ai's
|
|
5
|
+
* `api/simple-options`) and `estimateMessageTokens` (from `utils/estimate`) to
|
|
6
|
+
* predict the exact `max_tokens` pi's `completeSimple` will send. Importing
|
|
7
|
+
* those deep subpaths directly breaks under the bundled app: pi's extension
|
|
8
|
+
* loader takes an `alias` branch when pi is imported as a library (not the CLI),
|
|
9
|
+
* and jiti aliases match `@earendil-works/pi-ai` by prefix, rewriting every
|
|
10
|
+
* `@earendil-works/pi-ai/<subpath>` onto pi-ai's compat entry — so
|
|
11
|
+
* `@earendil-works/pi-ai/api/simple-options` resolves to the nonexistent
|
|
12
|
+
* `compat.js/api/simple-options` and the app cannot load the extension set
|
|
13
|
+
* (distribution review 2026-09-09, H1). Only the bare specifier and `/compat`,
|
|
14
|
+
* `/oauth`, `/providers/all` are aliased exactly; every other subpath is dead
|
|
15
|
+
* under the app.
|
|
16
|
+
*
|
|
17
|
+
* So the two consumed helpers (`clampMaxTokensToContext`, `estimateMessageTokens`)
|
|
18
|
+
* and the private estimator chain they depend on are copied here from pi-ai
|
|
19
|
+
* 0.85.0, importing only types from the bare specifier. The two exported helpers
|
|
20
|
+
* are byte-identical to upstream; the one adaptation is the internal
|
|
21
|
+
* `estimateContextTokens`, narrowed to accept only `Context` (the sole shape we
|
|
22
|
+
* call it with) rather than upstream's `Context | Message[]` overload — the
|
|
23
|
+
* Message[] branch would be dead code here. `test/unit/pi-ai-estimate.test.ts`
|
|
24
|
+
* imports the real deep paths from node_modules and asserts the two exported
|
|
25
|
+
* helpers produce identical numbers, so an upstream change is caught rather than
|
|
26
|
+
* silently drifting.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { Api, Context, ImageContent, Message, Model, TextContent, Tool, Usage } from "@earendil-works/pi-ai";
|
|
30
|
+
|
|
31
|
+
const CHARS_PER_TOKEN = 4;
|
|
32
|
+
const ESTIMATED_IMAGE_CHARS = 4800;
|
|
33
|
+
const CONTEXT_SAFETY_TOKENS = 4096;
|
|
34
|
+
const MIN_MAX_TOKENS = 1;
|
|
35
|
+
|
|
36
|
+
function calculateContextTokens(usage: Usage): number {
|
|
37
|
+
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function safeJsonStringify(value: unknown): string {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.stringify(value) ?? "undefined";
|
|
43
|
+
} catch {
|
|
44
|
+
return "[unserializable]";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function estimateTextAndImageContentChars(content: string | Array<TextContent | ImageContent>): number {
|
|
49
|
+
if (typeof content === "string") return content.length;
|
|
50
|
+
let chars = 0;
|
|
51
|
+
for (const block of content) chars += block.type === "text" ? block.text.length : ESTIMATED_IMAGE_CHARS;
|
|
52
|
+
return chars;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function estimateTextTokens(text: string): number {
|
|
56
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function estimateTextAndImageContentTokens(content: string | Array<TextContent | ImageContent>): number {
|
|
60
|
+
return Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function estimateMessageTokens(message: Message): number {
|
|
64
|
+
let chars = 0;
|
|
65
|
+
if (message.role === "user") return estimateTextAndImageContentTokens(message.content);
|
|
66
|
+
if (message.role === "toolResult") return estimateTextAndImageContentTokens(message.content);
|
|
67
|
+
for (const block of message.content) {
|
|
68
|
+
if (block.type === "text") {
|
|
69
|
+
chars += block.text.length;
|
|
70
|
+
} else if (block.type === "thinking") {
|
|
71
|
+
chars += block.thinking.length;
|
|
72
|
+
} else {
|
|
73
|
+
chars += block.name.length + safeJsonStringify(block.arguments).length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ContextUsageEstimate {
|
|
80
|
+
tokens: number;
|
|
81
|
+
usageTokens: number;
|
|
82
|
+
trailingTokens: number;
|
|
83
|
+
lastUsageIndex: number | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getLastAssistantUsageInfo(messages: readonly Message[]): { usage: Usage; index: number } | undefined {
|
|
87
|
+
let latestPrefixTimestamp = Number.NEGATIVE_INFINITY;
|
|
88
|
+
let usageInfo: { usage: Usage; index: number } | undefined;
|
|
89
|
+
for (let i = 0; i < messages.length; i++) {
|
|
90
|
+
const message = messages[i];
|
|
91
|
+
if (message.role === "assistant") {
|
|
92
|
+
const assistant = message;
|
|
93
|
+
// A newer prefix message was inserted after this response (for example, a
|
|
94
|
+
// compaction summary), so its usage cannot describe the current prefix.
|
|
95
|
+
const usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;
|
|
96
|
+
if (
|
|
97
|
+
usageAppliesToPrefix &&
|
|
98
|
+
assistant.stopReason !== "aborted" &&
|
|
99
|
+
assistant.stopReason !== "error" &&
|
|
100
|
+
calculateContextTokens(assistant.usage) > 0
|
|
101
|
+
) {
|
|
102
|
+
usageInfo = { usage: assistant.usage, index: i };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
latestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);
|
|
106
|
+
}
|
|
107
|
+
return usageInfo;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function estimateMessages(messages: readonly Message[]): ContextUsageEstimate {
|
|
111
|
+
const usageInfo = getLastAssistantUsageInfo(messages);
|
|
112
|
+
if (usageInfo) {
|
|
113
|
+
const usageTokens = calculateContextTokens(usageInfo.usage);
|
|
114
|
+
let trailingTokens = 0;
|
|
115
|
+
for (let i = usageInfo.index + 1; i < messages.length; i++) {
|
|
116
|
+
trailingTokens += estimateMessageTokens(messages[i]);
|
|
117
|
+
}
|
|
118
|
+
return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };
|
|
119
|
+
}
|
|
120
|
+
let tokens = 0;
|
|
121
|
+
for (const message of messages) tokens += estimateMessageTokens(message);
|
|
122
|
+
return { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function estimateToolsTokens(tools: Tool[] | undefined): number {
|
|
126
|
+
if (!tools || tools.length === 0) return 0;
|
|
127
|
+
return estimateTextTokens(safeJsonStringify(tools));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function estimateContextTokens(context: Context): ContextUsageEstimate {
|
|
131
|
+
const estimate = estimateMessages(context.messages);
|
|
132
|
+
if (estimate.lastUsageIndex !== null) {
|
|
133
|
+
const addedNames = new Set(
|
|
134
|
+
context.messages
|
|
135
|
+
.slice(estimate.lastUsageIndex + 1)
|
|
136
|
+
.filter((message) => message.role === "toolResult")
|
|
137
|
+
.flatMap((message) => (message.role === "toolResult" ? message.addedToolNames ?? [] : [])),
|
|
138
|
+
);
|
|
139
|
+
const addedToolTokens = estimateToolsTokens(context.tools?.filter((tool) => addedNames.has(tool.name)));
|
|
140
|
+
return {
|
|
141
|
+
tokens: estimate.tokens + addedToolTokens,
|
|
142
|
+
usageTokens: estimate.usageTokens,
|
|
143
|
+
trailingTokens: estimate.trailingTokens + addedToolTokens,
|
|
144
|
+
lastUsageIndex: estimate.lastUsageIndex,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const prefixTokens = (context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0) + estimateToolsTokens(context.tools);
|
|
148
|
+
return {
|
|
149
|
+
tokens: estimate.tokens + prefixTokens,
|
|
150
|
+
usageTokens: estimate.usageTokens,
|
|
151
|
+
trailingTokens: estimate.trailingTokens + prefixTokens,
|
|
152
|
+
lastUsageIndex: estimate.lastUsageIndex,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function clampMaxTokensToContext(model: Model<Api>, context: Context, maxTokens: number): number {
|
|
157
|
+
if (model.contextWindow <= 0) return Math.max(MIN_MAX_TOKENS, maxTokens);
|
|
158
|
+
const available = model.contextWindow - estimateContextTokens(context).tokens - CONTEXT_SAFETY_TOKENS;
|
|
159
|
+
return Math.min(maxTokens, Math.max(MIN_MAX_TOKENS, available));
|
|
160
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
/** Inclusive minimum and exclusive maximum pi version this release is tested against. */
|
|
14
14
|
export const TESTED_PI_MIN = "0.83.0";
|
|
15
|
-
export const TESTED_PI_MAX_EXCLUSIVE = "0.
|
|
15
|
+
export const TESTED_PI_MAX_EXCLUSIVE = "0.86.0";
|
|
16
16
|
|
|
17
17
|
/** Dotted-numeric parse; undefined for anything that is not plain x.y.z numbers. */
|
|
18
18
|
export function parseVersion(version: string): number[] | undefined {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bus channels between plan-mode and permissions. They live here, not in
|
|
3
|
+
* either extension, because permissions cannot import plan-mode (plan-mode
|
|
4
|
+
* already imports `permissions/modes.ts`) and a string literal typed in two
|
|
5
|
+
* places is the one way a channel rename silently disconnects a listener.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Request a permission-mode change (`{ mode }`); permissions applies it. */
|
|
9
|
+
export const MODE_CHANNEL = "one-code:set-permission-mode";
|
|
10
|
+
/** Announces plan mode's one writable file (`{ path }`); the permissions matcher consumes it. */
|
|
11
|
+
export const PLAN_FILE_CHANNEL = "one-code:plan-file-path";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Code-local enable/disable overrides for Claude Code-installed plugins
|
|
3
|
+
* (pure fs, no pi imports).
|
|
4
|
+
*
|
|
5
|
+
* A plugin installed under ~/.claude keeps its enabled state in Claude Code's
|
|
6
|
+
* own settings.json, which One Code never writes. Toggling such a plugin from
|
|
7
|
+
* One Code therefore records an override here (`<plugin root>/overrides.json`,
|
|
8
|
+
* `{"name@marketplace": boolean}`), applied on top of Claude Code's state
|
|
9
|
+
* during discovery. Plugins installed by One Code itself store `enabled`
|
|
10
|
+
* directly on their installed_plugins.json entry and never appear here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { readBooleanMap, setBooleanMapEntry } from "./atomic-write.ts";
|
|
15
|
+
|
|
16
|
+
const FILE = "overrides.json";
|
|
17
|
+
|
|
18
|
+
export function readOverrides(root: string): Record<string, boolean> {
|
|
19
|
+
return readBooleanMap(join(root, FILE));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setOverride(root: string, pluginId: string, enabled: boolean): void {
|
|
23
|
+
setBooleanMapEntry(join(root, FILE), pluginId, enabled);
|
|
24
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Code's own plugin state root (pure).
|
|
3
|
+
*
|
|
4
|
+
* All plugin state One Code writes — marketplaces, installs, overrides, usage,
|
|
5
|
+
* favorites — lives under `<agentDir>/plugins`, never under `~/.claude`.
|
|
6
|
+
* `~/.claude/plugins` is a read-only input (plugins installed by Claude Code
|
|
7
|
+
* keep working read-through; see lib/plugins.ts).
|
|
8
|
+
*
|
|
9
|
+
* Callers pass `getAgentDir()` so the root follows the distribution mode:
|
|
10
|
+
* `~/.onecode/agent/plugins` under the bundled app (PI_CODING_AGENT_DIR),
|
|
11
|
+
* `~/.pi/agent/plugins` when running as a plain pi extension.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join, relative, resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
export function pluginRoot(agentDir: string): string {
|
|
17
|
+
return join(agentDir, "plugins");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One path segment safe for any untrusted plugin/marketplace name (matches
|
|
22
|
+
* Claude Code's sanitizer; versions keep their dots). Never empty — an
|
|
23
|
+
* all-symbol name must not collapse to the parent directory.
|
|
24
|
+
*/
|
|
25
|
+
export function sanitizePathSegment(value: string, allowDots = false): string {
|
|
26
|
+
const pattern = allowDots ? /[^a-zA-Z0-9\-_.]/g : /[^a-zA-Z0-9\-_]/g;
|
|
27
|
+
const cleaned = value.replace(pattern, "-");
|
|
28
|
+
// A dots-only result (`.`, `..`, `...`) is not a directory name: `join`
|
|
29
|
+
// normalises it and the segment escapes into the parent — a `version: ".."`
|
|
30
|
+
// would install into the cache root and wipe siblings (review M4). Treat any
|
|
31
|
+
// all-dots segment as empty.
|
|
32
|
+
if (cleaned.length === 0 || /^\.+$/.test(cleaned)) return "unnamed";
|
|
33
|
+
return cleaned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when `target` resolves strictly inside `base` — the containment check
|
|
38
|
+
* for third-party-authored relative paths (rejects `..` escapes and absolute
|
|
39
|
+
* overrides).
|
|
40
|
+
*/
|
|
41
|
+
export function pathWithinBase(base: string, target: string): boolean {
|
|
42
|
+
const rel = relative(resolve(base), resolve(base, target));
|
|
43
|
+
return rel !== "" && !rel.startsWith("..") && resolve(rel) !== rel;
|
|
44
|
+
}
|