@parall/claude-agent 1.42.1 → 1.44.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.
@@ -18,11 +18,20 @@ type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allo
18
18
  * resolve no_action instead of reply-effect dedupe).
19
19
  */
20
20
  contextDirPath?: string;
21
+ /**
22
+ * Channel-capability shim directory (agent-core's capabilityBinDir),
23
+ * prepended to the subprocess PATH unconditionally. The DIRECTORY is
24
+ * constant for the bridge's lifetime; its CONTENT tracks capability
25
+ * grants, so a grant or revocation reaches even a long-lived claude
26
+ * subprocess on its next shell command — no respawn needed.
27
+ */
28
+ capabilityBinDir?: string;
21
29
  };
22
30
  export declare function buildSpawnEnv(parentEnv: NodeJS.ProcessEnv, claudeHome: string, context: DispatchOpts['context'], opts: {
23
31
  allowApiKey: boolean;
24
32
  effortLevel?: string;
25
33
  wikiMountRoot?: string;
34
+ capabilityBinDir?: string;
26
35
  }): NodeJS.ProcessEnv;
27
36
  export declare class ClaudeCodeAdapter implements DispatchAdapter {
28
37
  private readonly opts;
@@ -38,6 +47,7 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
38
47
  model?: string | null;
39
48
  effort?: string | null;
40
49
  }): void;
50
+ requestProcessRestart(): void;
41
51
  enqueueDuringDispatch(sessionKey: string, body: string): boolean;
42
52
  abortDispatch(sessionKey: string): void;
43
53
  hasPendingInjections(sessionKey: string): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
package/dist/dispatch.js CHANGED
@@ -52,6 +52,23 @@ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
52
52
  if (opts.effortLevel) {
53
53
  result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
54
54
  }
55
+ // Channel-capability shims resolve ahead of any globally-installed CLI of
56
+ // the same name (the shim owns auth; the real binary is found further down
57
+ // the PATH by the shim itself). On Windows env keys are case-insensitive and
58
+ // the host key is usually `Path`; after the plain-object spread a literal
59
+ // `PATH` write would SPLIT the key (undefined which wins in the child), so
60
+ // reuse the parent's casing. On POSIX keys are case-SENSITIVE — `PATH` is
61
+ // the real one and a stray `Path` is unrelated, so match exactly there or we
62
+ // could prepend to the wrong variable and silently bypass the shim.
63
+ if (opts.capabilityBinDir) {
64
+ const pathKey = process.platform === 'win32'
65
+ ? (Object.keys(result).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
66
+ : 'PATH';
67
+ const existing = result[pathKey];
68
+ result[pathKey] = existing
69
+ ? `${opts.capabilityBinDir}${path.delimiter}${existing}`
70
+ : opts.capabilityBinDir;
71
+ }
55
72
  return result;
56
73
  }
57
74
  export class ClaudeCodeAdapter {
@@ -79,9 +96,18 @@ export class ClaudeCodeAdapter {
79
96
  if (effortChanged)
80
97
  this._effortLevel = config.effort ?? undefined;
81
98
  if (modelChanged || effortChanged) {
82
- for (const [, state] of this.processes) {
83
- state.needsRestart = true;
84
- }
99
+ this.requestProcessRestart();
100
+ }
101
+ }
102
+ // Mark every live subprocess for a lazy respawn (applied at its next
103
+ // dispatch; the session survives via --resume) — the same mechanism
104
+ // updateConfig uses for model/effort changes. For spawn-time inputs that
105
+ // live OUTSIDE this adapter's config, e.g. the .parall/system-prompt.md
106
+ // file: a long-lived process read it via --append-system-prompt-file at
107
+ // spawn, so a capability declaration change only reaches it by respawning.
108
+ requestProcessRestart() {
109
+ for (const [, state] of this.processes) {
110
+ state.needsRestart = true;
85
111
  }
86
112
  }
87
113
  enqueueDuringDispatch(sessionKey, body) {
@@ -389,6 +415,7 @@ export class ClaudeCodeAdapter {
389
415
  allowApiKey: this.opts.allowApiKey,
390
416
  effortLevel: this._effortLevel,
391
417
  wikiMountRoot: this.opts.workspaceDir,
418
+ capabilityBinDir: this.opts.capabilityBinDir,
392
419
  });
393
420
  log?.info(`spawn long-lived ${this.opts.claudeBin} (session ${sessionKey}, model=${this._model || 'default'}, effort=${this._effortLevel || 'default'}, mode=${this.opts.permissionMode})`);
394
421
  const proc = spawn(IS_WIN32 ? quoteWin32Arg(this.opts.claudeBin) : this.opts.claudeBin, IS_WIN32 ? args.map(quoteWin32Arg) : args, {
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import * as os from 'node:os';
3
- import { ParallAgentGateway, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
3
+ import { ParallAgentGateway, capabilityBinDir, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
4
4
  import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
5
5
  import { buildClaudeRuntimeKey, contextFilePathForSession, dispatchContextDirPath, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from './config.js';
6
6
  import { ClaudeCodeAdapter } from './dispatch.js';
7
7
  import { ClaudeSessionManager } from './session-manager.js';
8
- import { ensureClaudeWorkspace } from './workspace.js';
8
+ import { ensureClaudeWorkspace, writeClaudeSystemPrompt } from './workspace.js';
9
9
  const log = createLogger('claude-agent');
10
10
  let activeLog = log;
11
11
  async function getAgentMeWithLegacyFallback(client, orgId) {
@@ -61,11 +61,13 @@ async function main() {
61
61
  const agentUserId = me.id;
62
62
  const agentLog = childLogger(activeLog, agentUserId);
63
63
  activeLog = agentLog;
64
- ensureClaudeWorkspace(config.workspaceDir, agentLog, {
64
+ // Shared by the initial workspace write and the config-refresh rewrite of
65
+ // .parall/system-prompt.md — both need identity + capability fragments.
66
+ const agentIdentity = {
65
67
  userId: agentUserId,
66
68
  displayName: me.display_name,
67
69
  description: me.agent_profile?.description ?? undefined,
68
- });
70
+ };
69
71
  const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
70
72
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
71
73
  const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
@@ -81,6 +83,22 @@ async function main() {
81
83
  log: agentLog,
82
84
  });
83
85
  const platformDefaults = await configMgr.fetch();
86
+ // Channel capabilities: ONE call materializes the local artifacts (the
87
+ // lark-cli auth shim under capabilityBinDir) and yields the system-prompt
88
+ // fragments, so declaration and shim can never diverge within a refresh.
89
+ // No freshness gate — a stale (LKG-cached) declaration fail-closes at the
90
+ // mint endpoint, which re-evaluates the grant on every token request.
91
+ const applyChannelCapabilities = () => {
92
+ const caps = configMgr.capabilities();
93
+ materializeChannelCapabilities(config.stateDir, caps, agentLog);
94
+ return caps.map((c) => c.fragment);
95
+ };
96
+ // Assemble the workspace AFTER the first config fetch so system-prompt.md
97
+ // carries the capability declarations from boot (not just from the first
98
+ // heat-update).
99
+ const bootCapabilityFragments = applyChannelCapabilities();
100
+ let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
101
+ ensureClaudeWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
84
102
  // Model precedence: operator PIN (override) > env > server FLOOR. The server
85
103
  // now says which it is via model_is_pin (deriveModelIsPin presence-gates the
86
104
  // dual-read: old servers omit it → fall back to model_management). A PIN beats
@@ -115,6 +133,9 @@ async function main() {
115
133
  contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
116
134
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
117
135
  contextDirPath: dispatchContextDirPath(config.stateDir),
136
+ // Constant PATH prepend; the directory's content (the lark-cli shim)
137
+ // tracks capability grants, so add/remove reaches live subprocesses.
138
+ capabilityBinDir: capabilityBinDir(config.stateDir),
118
139
  });
119
140
  if (resolvedEffort) {
120
141
  adapter.updateConfig({ effort: resolvedEffort });
@@ -144,6 +165,39 @@ async function main() {
144
165
  model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
145
166
  effort: updated.thinkingEffort ?? localEffort,
146
167
  });
168
+ // Channel capabilities heat-update with the config: re-materialize the
169
+ // shim and rewrite the standing system prompt on EVERY refresh. The
170
+ // rewrite is unconditional on purpose — it is self-healing: the prompt
171
+ // file lives in the agent-writable workspace, so a subprocess or its
172
+ // tools can delete or tamper with it, and rewriting each refresh/reconnect
173
+ // restores the authoritative copy (do NOT gate this on a fragment change).
174
+ // Revocation keeps the constant pointer in place (no deny stub, no local
175
+ // token cache): the mint endpoint's 403 IS the revocation, nothing local
176
+ // to purge. The shim swap reaches even live long-lived subprocesses
177
+ // immediately (PATH re-resolves per command), but the system prompt was
178
+ // loaded at spawn — so a CHANGED declaration also marks live processes for
179
+ // a lazy --resume respawn, the same mechanism model changes use.
180
+ const fragments = applyChannelCapabilities();
181
+ const joinedFragments = fragments.join('\n\n');
182
+ // Refresh path: TOLERATE a prompt-write failure (boot is fatal instead) —
183
+ // a transient failure must not break the whole config heat-update. But do
184
+ // NOT advance the snapshot / restart unless the write actually LANDED:
185
+ // otherwise a later successful retry sees "no change" and never restarts
186
+ // the long-lived Claude subprocess, stranding it on the stale prompt. The
187
+ // rewrite is self-healing; leaving the snapshot put makes the next refresh
188
+ // retry and, on success, restart.
189
+ let promptWritten = true;
190
+ try {
191
+ writeClaudeSystemPrompt(config.workspaceDir, agentIdentity, fragments);
192
+ }
193
+ catch (err) {
194
+ promptWritten = false;
195
+ agentLog.warn(`system prompt refresh write failed (retrying next refresh): ${String(err)}`);
196
+ }
197
+ if (promptWritten && joinedFragments !== lastCapabilityFragments) {
198
+ lastCapabilityFragments = joinedFragments;
199
+ adapter.requestProcessRestart();
200
+ }
147
201
  };
148
202
  const gateway = new ParallAgentGateway({
149
203
  accountId: agentUserId,
@@ -166,6 +220,11 @@ async function main() {
166
220
  },
167
221
  dispatchAdapter: adapter,
168
222
  log: agentLog,
223
+ // Live capability view for hint routing: the channel reply hint points
224
+ // at the vendor CLI only while the grant is active (config manager
225
+ // re-fetches on agent_config.update, so revocation reaches the very
226
+ // next dispatch's hint).
227
+ getCapabilityKeys: () => configMgr.capabilities().map((c) => c.key),
169
228
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
170
229
  forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
171
230
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
@@ -1,5 +1,6 @@
1
1
  import type { AgentIdentity } from '@parall/agent-core';
2
+ export declare function writeClaudeSystemPrompt(workspaceDir: string, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): void;
2
3
  export declare function ensureClaudeWorkspace(workspaceDir: string, log?: {
3
4
  warn: (msg: string) => void;
4
- }, agentIdentity?: AgentIdentity): void;
5
+ }, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): void;
5
6
  //# sourceMappingURL=workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,QAmB9B"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA8BxD,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,aAAa,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,QAgB/B;AAED,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,QAW/B"}
package/dist/workspace.js CHANGED
@@ -2,19 +2,46 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { BRIDGE_WORKSPACE_INSTRUCTIONS, PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity, buildSkillReferences, writeSkillFiles, } from '@parall/agent-core';
4
4
  import { ensureLocalAttachmentGitExclude } from '@parall/agent-core/internal/attachment-input';
5
- export function ensureClaudeWorkspace(workspaceDir, log, agentIdentity) {
6
- const systemPrompt = [
5
+ // buildClaudeSystemPrompt assembles the standing system prompt. capabilityFragments
6
+ // are platform-derived capability declarations (agents.capabilities[].fragment
7
+ // from platform-config) — placed after the platform reference guide, before
8
+ // skill references. Empty/absent = no capability section.
9
+ function buildClaudeSystemPrompt(workspaceDir, agentIdentity, capabilityFragments) {
10
+ const parts = [
7
11
  buildIdentity(agentIdentity),
8
12
  BRIDGE_WORKSPACE_INSTRUCTIONS,
9
13
  PRLL_BEHAVIOR,
10
14
  PRLL_REFERENCE_GUIDE,
11
- buildSkillReferences(workspaceDir),
12
- ].join('\n\n');
13
- fs.mkdirSync(workspaceDir, { recursive: true });
14
- fs.mkdirSync(path.join(workspaceDir, '.claude'), { recursive: true });
15
+ ];
16
+ if (capabilityFragments && capabilityFragments.length > 0) {
17
+ parts.push(capabilityFragments.join('\n\n'));
18
+ }
19
+ parts.push(buildSkillReferences(workspaceDir));
20
+ return parts.join('\n\n');
21
+ }
22
+ // writeClaudeSystemPrompt (re)writes .parall/system-prompt.md. Split out from
23
+ // ensureClaudeWorkspace so the bridge can rewrite it when platform-config
24
+ // capabilities heat-update — the file is read via --append-system-prompt-file
25
+ // on every claude spawn, so a rewrite reaches the next turn without a
26
+ // workspace rebuild.
27
+ export function writeClaudeSystemPrompt(workspaceDir, agentIdentity, capabilityFragments) {
28
+ // Throws on failure BY DESIGN — tolerance is a property of the CALLER, not
29
+ // this function. At boot (ensureClaudeWorkspace) the prompt is mandatory
30
+ // (every claude spawn passes it via --append-system-prompt-file), so a write
31
+ // failure must fail startup loudly for recovery, not degrade to an agent
32
+ // running without its platform instructions. The refresh hot path (index.ts)
33
+ // wraps this in try/catch instead, so a transient failure there degrades to a
34
+ // warning and the next refresh retries (the rewrite is self-healing).
15
35
  const parallDir = path.join(workspaceDir, '.parall');
16
36
  fs.mkdirSync(parallDir, { recursive: true });
17
- fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), systemPrompt, 'utf8');
18
- writeSkillFiles(path.join(parallDir, 'skills'));
37
+ fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), buildClaudeSystemPrompt(workspaceDir, agentIdentity, capabilityFragments), 'utf8');
38
+ }
39
+ export function ensureClaudeWorkspace(workspaceDir, log, agentIdentity, capabilityFragments) {
40
+ fs.mkdirSync(workspaceDir, { recursive: true });
41
+ fs.mkdirSync(path.join(workspaceDir, '.claude'), { recursive: true });
42
+ // Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
43
+ // without the mandatory system prompt. The refresh path tolerates failure.
44
+ writeClaudeSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
45
+ writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
19
46
  ensureLocalAttachmentGitExclude(workspaceDir);
20
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.42.1",
3
+ "version": "1.44.0",
4
4
  "description": "Claude Code bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "src"
26
26
  ],
27
27
  "dependencies": {
28
- "@parall/cli": "1.42.1",
29
- "@parall/sdk": "1.42.1",
30
- "@parall/agent-core": "1.42.1"
28
+ "@parall/agent-core": "1.44.0",
29
+ "@parall/cli": "1.44.0",
30
+ "@parall/sdk": "1.44.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -47,6 +47,14 @@ type ClaudeCodeAdapterOptions = Pick<
47
47
  * resolve no_action instead of reply-effect dedupe).
48
48
  */
49
49
  contextDirPath?: string;
50
+ /**
51
+ * Channel-capability shim directory (agent-core's capabilityBinDir),
52
+ * prepended to the subprocess PATH unconditionally. The DIRECTORY is
53
+ * constant for the bridge's lifetime; its CONTENT tracks capability
54
+ * grants, so a grant or revocation reaches even a long-lived claude
55
+ * subprocess on its next shell command — no respawn needed.
56
+ */
57
+ capabilityBinDir?: string;
50
58
  };
51
59
 
52
60
  const IS_WIN32 = process.platform === 'win32';
@@ -69,7 +77,12 @@ export function buildSpawnEnv(
69
77
  parentEnv: NodeJS.ProcessEnv,
70
78
  claudeHome: string,
71
79
  context: DispatchOpts['context'],
72
- opts: { allowApiKey: boolean; effortLevel?: string; wikiMountRoot?: string },
80
+ opts: {
81
+ allowApiKey: boolean;
82
+ effortLevel?: string;
83
+ wikiMountRoot?: string;
84
+ capabilityBinDir?: string;
85
+ },
73
86
  ): NodeJS.ProcessEnv {
74
87
  const env: NodeJS.ProcessEnv = { ...parentEnv };
75
88
  if (!opts.allowApiKey) {
@@ -103,6 +116,24 @@ export function buildSpawnEnv(
103
116
  if (opts.effortLevel) {
104
117
  result.CLAUDE_CODE_EFFORT_LEVEL = opts.effortLevel;
105
118
  }
119
+ // Channel-capability shims resolve ahead of any globally-installed CLI of
120
+ // the same name (the shim owns auth; the real binary is found further down
121
+ // the PATH by the shim itself). On Windows env keys are case-insensitive and
122
+ // the host key is usually `Path`; after the plain-object spread a literal
123
+ // `PATH` write would SPLIT the key (undefined which wins in the child), so
124
+ // reuse the parent's casing. On POSIX keys are case-SENSITIVE — `PATH` is
125
+ // the real one and a stray `Path` is unrelated, so match exactly there or we
126
+ // could prepend to the wrong variable and silently bypass the shim.
127
+ if (opts.capabilityBinDir) {
128
+ const pathKey =
129
+ process.platform === 'win32'
130
+ ? (Object.keys(result).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
131
+ : 'PATH';
132
+ const existing = result[pathKey];
133
+ result[pathKey] = existing
134
+ ? `${opts.capabilityBinDir}${path.delimiter}${existing}`
135
+ : opts.capabilityBinDir;
136
+ }
106
137
  return result;
107
138
  }
108
139
 
@@ -145,9 +176,19 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
145
176
  if (modelChanged) this._model = config.model ?? undefined;
146
177
  if (effortChanged) this._effortLevel = config.effort ?? undefined;
147
178
  if (modelChanged || effortChanged) {
148
- for (const [, state] of this.processes) {
149
- state.needsRestart = true;
150
- }
179
+ this.requestProcessRestart();
180
+ }
181
+ }
182
+
183
+ // Mark every live subprocess for a lazy respawn (applied at its next
184
+ // dispatch; the session survives via --resume) — the same mechanism
185
+ // updateConfig uses for model/effort changes. For spawn-time inputs that
186
+ // live OUTSIDE this adapter's config, e.g. the .parall/system-prompt.md
187
+ // file: a long-lived process read it via --append-system-prompt-file at
188
+ // spawn, so a capability declaration change only reaches it by respawning.
189
+ requestProcessRestart(): void {
190
+ for (const [, state] of this.processes) {
191
+ state.needsRestart = true;
151
192
  }
152
193
  }
153
194
 
@@ -493,6 +534,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
493
534
  allowApiKey: this.opts.allowApiKey,
494
535
  effortLevel: this._effortLevel,
495
536
  wikiMountRoot: this.opts.workspaceDir,
537
+ capabilityBinDir: this.opts.capabilityBinDir,
496
538
  },
497
539
  );
498
540
 
package/src/index.ts CHANGED
@@ -3,11 +3,13 @@
3
3
  import * as os from 'node:os';
4
4
  import {
5
5
  ParallAgentGateway,
6
+ capabilityBinDir,
6
7
  createPlatformConfigManager,
7
8
  createLogger,
8
9
  createOtelLogger,
9
10
  childLogger,
10
11
  deriveModelIsPin,
12
+ materializeChannelCapabilities,
11
13
  resolveRuntimeModel,
12
14
  parseShutdownDeadlineMs,
13
15
  parseForkDeadlineMs,
@@ -29,7 +31,7 @@ import {
29
31
  } from './config.js';
30
32
  import { ClaudeCodeAdapter } from './dispatch.js';
31
33
  import { ClaudeSessionManager } from './session-manager.js';
32
- import { ensureClaudeWorkspace } from './workspace.js';
34
+ import { ensureClaudeWorkspace, writeClaudeSystemPrompt } from './workspace.js';
33
35
 
34
36
  const log = createLogger('claude-agent');
35
37
  let activeLog = log;
@@ -90,11 +92,13 @@ async function main() {
90
92
  const agentUserId = me.id;
91
93
  const agentLog = childLogger(activeLog, agentUserId);
92
94
  activeLog = agentLog;
93
- ensureClaudeWorkspace(config.workspaceDir, agentLog, {
95
+ // Shared by the initial workspace write and the config-refresh rewrite of
96
+ // .parall/system-prompt.md — both need identity + capability fragments.
97
+ const agentIdentity = {
94
98
  userId: agentUserId,
95
99
  displayName: me.display_name,
96
100
  description: me.agent_profile?.description ?? undefined,
97
- });
101
+ };
98
102
  const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
99
103
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
100
104
  const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
@@ -112,6 +116,22 @@ async function main() {
112
116
  log: agentLog,
113
117
  });
114
118
  const platformDefaults = await configMgr.fetch();
119
+ // Channel capabilities: ONE call materializes the local artifacts (the
120
+ // lark-cli auth shim under capabilityBinDir) and yields the system-prompt
121
+ // fragments, so declaration and shim can never diverge within a refresh.
122
+ // No freshness gate — a stale (LKG-cached) declaration fail-closes at the
123
+ // mint endpoint, which re-evaluates the grant on every token request.
124
+ const applyChannelCapabilities = () => {
125
+ const caps = configMgr.capabilities();
126
+ materializeChannelCapabilities(config.stateDir, caps, agentLog);
127
+ return caps.map((c) => c.fragment);
128
+ };
129
+ // Assemble the workspace AFTER the first config fetch so system-prompt.md
130
+ // carries the capability declarations from boot (not just from the first
131
+ // heat-update).
132
+ const bootCapabilityFragments = applyChannelCapabilities();
133
+ let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
134
+ ensureClaudeWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
115
135
  // Model precedence: operator PIN (override) > env > server FLOOR. The server
116
136
  // now says which it is via model_is_pin (deriveModelIsPin presence-gates the
117
137
  // dual-read: old servers omit it → fall back to model_management). A PIN beats
@@ -156,6 +176,9 @@ async function main() {
156
176
  stepIdFilePathForSession: (sessionKey) =>
157
177
  stepIdFilePathForSession(config.stateDir, sessionKey),
158
178
  contextDirPath: dispatchContextDirPath(config.stateDir),
179
+ // Constant PATH prepend; the directory's content (the lark-cli shim)
180
+ // tracks capability grants, so add/remove reaches live subprocesses.
181
+ capabilityBinDir: capabilityBinDir(config.stateDir),
159
182
  });
160
183
  if (resolvedEffort) {
161
184
  adapter.updateConfig({ effort: resolvedEffort });
@@ -189,6 +212,38 @@ async function main() {
189
212
  model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
190
213
  effort: updated.thinkingEffort ?? localEffort,
191
214
  });
215
+ // Channel capabilities heat-update with the config: re-materialize the
216
+ // shim and rewrite the standing system prompt on EVERY refresh. The
217
+ // rewrite is unconditional on purpose — it is self-healing: the prompt
218
+ // file lives in the agent-writable workspace, so a subprocess or its
219
+ // tools can delete or tamper with it, and rewriting each refresh/reconnect
220
+ // restores the authoritative copy (do NOT gate this on a fragment change).
221
+ // Revocation keeps the constant pointer in place (no deny stub, no local
222
+ // token cache): the mint endpoint's 403 IS the revocation, nothing local
223
+ // to purge. The shim swap reaches even live long-lived subprocesses
224
+ // immediately (PATH re-resolves per command), but the system prompt was
225
+ // loaded at spawn — so a CHANGED declaration also marks live processes for
226
+ // a lazy --resume respawn, the same mechanism model changes use.
227
+ const fragments = applyChannelCapabilities();
228
+ const joinedFragments = fragments.join('\n\n');
229
+ // Refresh path: TOLERATE a prompt-write failure (boot is fatal instead) —
230
+ // a transient failure must not break the whole config heat-update. But do
231
+ // NOT advance the snapshot / restart unless the write actually LANDED:
232
+ // otherwise a later successful retry sees "no change" and never restarts
233
+ // the long-lived Claude subprocess, stranding it on the stale prompt. The
234
+ // rewrite is self-healing; leaving the snapshot put makes the next refresh
235
+ // retry and, on success, restart.
236
+ let promptWritten = true;
237
+ try {
238
+ writeClaudeSystemPrompt(config.workspaceDir, agentIdentity, fragments);
239
+ } catch (err) {
240
+ promptWritten = false;
241
+ agentLog.warn(`system prompt refresh write failed (retrying next refresh): ${String(err)}`);
242
+ }
243
+ if (promptWritten && joinedFragments !== lastCapabilityFragments) {
244
+ lastCapabilityFragments = joinedFragments;
245
+ adapter.requestProcessRestart();
246
+ }
192
247
  };
193
248
 
194
249
  const gateway = new ParallAgentGateway({
@@ -212,6 +267,11 @@ async function main() {
212
267
  },
213
268
  dispatchAdapter: adapter,
214
269
  log: agentLog,
270
+ // Live capability view for hint routing: the channel reply hint points
271
+ // at the vendor CLI only while the grant is active (config manager
272
+ // re-fetches on agent_config.update, so revocation reaches the very
273
+ // next dispatch's hint).
274
+ getCapabilityKeys: () => configMgr.capabilities().map((c) => c.key),
215
275
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
216
276
  forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
217
277
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
package/src/workspace.ts CHANGED
@@ -11,26 +11,67 @@ import {
11
11
  import type { AgentIdentity } from '@parall/agent-core';
12
12
  import { ensureLocalAttachmentGitExclude } from '@parall/agent-core/internal/attachment-input';
13
13
 
14
- export function ensureClaudeWorkspace(
14
+ // buildClaudeSystemPrompt assembles the standing system prompt. capabilityFragments
15
+ // are platform-derived capability declarations (agents.capabilities[].fragment
16
+ // from platform-config) — placed after the platform reference guide, before
17
+ // skill references. Empty/absent = no capability section.
18
+ function buildClaudeSystemPrompt(
15
19
  workspaceDir: string,
16
- log?: { warn: (msg: string) => void },
17
20
  agentIdentity?: AgentIdentity,
18
- ) {
19
- const systemPrompt = [
21
+ capabilityFragments?: string[],
22
+ ): string {
23
+ const parts = [
20
24
  buildIdentity(agentIdentity),
21
25
  BRIDGE_WORKSPACE_INSTRUCTIONS,
22
26
  PRLL_BEHAVIOR,
23
27
  PRLL_REFERENCE_GUIDE,
24
- buildSkillReferences(workspaceDir),
25
- ].join('\n\n');
28
+ ];
29
+ if (capabilityFragments && capabilityFragments.length > 0) {
30
+ parts.push(capabilityFragments.join('\n\n'));
31
+ }
32
+ parts.push(buildSkillReferences(workspaceDir));
33
+ return parts.join('\n\n');
34
+ }
35
+
36
+ // writeClaudeSystemPrompt (re)writes .parall/system-prompt.md. Split out from
37
+ // ensureClaudeWorkspace so the bridge can rewrite it when platform-config
38
+ // capabilities heat-update — the file is read via --append-system-prompt-file
39
+ // on every claude spawn, so a rewrite reaches the next turn without a
40
+ // workspace rebuild.
41
+ export function writeClaudeSystemPrompt(
42
+ workspaceDir: string,
43
+ agentIdentity?: AgentIdentity,
44
+ capabilityFragments?: string[],
45
+ ) {
46
+ // Throws on failure BY DESIGN — tolerance is a property of the CALLER, not
47
+ // this function. At boot (ensureClaudeWorkspace) the prompt is mandatory
48
+ // (every claude spawn passes it via --append-system-prompt-file), so a write
49
+ // failure must fail startup loudly for recovery, not degrade to an agent
50
+ // running without its platform instructions. The refresh hot path (index.ts)
51
+ // wraps this in try/catch instead, so a transient failure there degrades to a
52
+ // warning and the next refresh retries (the rewrite is self-healing).
53
+ const parallDir = path.join(workspaceDir, '.parall');
54
+ fs.mkdirSync(parallDir, { recursive: true });
55
+ fs.writeFileSync(
56
+ path.join(parallDir, 'system-prompt.md'),
57
+ buildClaudeSystemPrompt(workspaceDir, agentIdentity, capabilityFragments),
58
+ 'utf8',
59
+ );
60
+ }
26
61
 
62
+ export function ensureClaudeWorkspace(
63
+ workspaceDir: string,
64
+ log?: { warn: (msg: string) => void },
65
+ agentIdentity?: AgentIdentity,
66
+ capabilityFragments?: string[],
67
+ ) {
27
68
  fs.mkdirSync(workspaceDir, { recursive: true });
28
69
  fs.mkdirSync(path.join(workspaceDir, '.claude'), { recursive: true });
29
70
 
30
- const parallDir = path.join(workspaceDir, '.parall');
31
- fs.mkdirSync(parallDir, { recursive: true });
32
- fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), systemPrompt, 'utf8');
71
+ // Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
72
+ // without the mandatory system prompt. The refresh path tolerates failure.
73
+ writeClaudeSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
33
74
 
34
- writeSkillFiles(path.join(parallDir, 'skills'));
75
+ writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
35
76
  ensureLocalAttachmentGitExclude(workspaceDir);
36
77
  }