brainclaw 1.12.0 → 1.13.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/README.md +32 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +9 -2
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/estimation-report.js +1 -1
- package/dist/commands/harvest.js +30 -22
- package/dist/commands/mcp.js +279 -44
- package/dist/commands/release-claim.js +21 -1
- package/dist/core/agent-capability.js +15 -4
- package/dist/core/agent-registry.js +7 -1
- package/dist/core/claims.js +160 -1
- package/dist/core/context.js +11 -4
- package/dist/core/dispatch-status.js +113 -7
- package/dist/core/entity-operations.js +51 -3
- package/dist/core/loops/store.js +33 -0
- package/dist/core/reputation.js +18 -0
- package/dist/core/schema.js +7 -0
- package/dist/core/worktree.js +146 -22
- package/dist/facts.js +36 -3
- package/dist/facts.json +35 -2
- package/docs/mcp-schema-changelog.md +7 -2
- package/package.json +6 -4
package/dist/commands/mcp.js
CHANGED
|
@@ -31,13 +31,14 @@ import { rejectCandidate } from './reject.js';
|
|
|
31
31
|
import { startSession } from './session-start.js';
|
|
32
32
|
import { endSession } from './session-end.js';
|
|
33
33
|
import { applyHandoffUpdates } from './update-handoff.js';
|
|
34
|
-
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, } from '../core/agent-registry.js';
|
|
34
|
+
import { AgentIdentityResolutionError, AgentTrustError, findAgentIdentityById, findAgentIdentityByName, hasMinimumTrustLevel, normalizeAgentName, requireMinimumTrustLevel, requireRegisteredAgentIdentity, resolveCurrentAgentIdentity, resolveCurrentModel, ensureAgentRegisteredForDispatch, resolveOrAutoRegisterAgentIdentity, } from '../core/agent-registry.js';
|
|
35
35
|
import { appendAuditEntry } from '../core/audit.js';
|
|
36
36
|
import { nowISO, generateId } from '../core/ids.js';
|
|
37
|
-
import { buildOperationalIdentity, loadAllSessions, loadSessionById } from '../core/identity.js';
|
|
37
|
+
import { buildOperationalIdentity, loadAllSessions, loadCurrentSession, loadSessionById, saveCurrentSession } from '../core/identity.js';
|
|
38
38
|
import { validateMcpInput, validateMcpField } from '../core/input-validation.js';
|
|
39
39
|
import { createCapability, createTool as createRegistryTool } from '../core/registries.js';
|
|
40
40
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
41
|
+
import { isObserverMode } from '../core/observer-mode.js';
|
|
41
42
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
42
43
|
import { buildAgentInventory } from '../core/agent-inventory.js';
|
|
43
44
|
import { findOutermostBrainclawRoot, resolveEffectiveCwd, resolveEffectiveCwdInfo, resolveProjectRef, resolveTargetStore } from '../core/store-resolution.js';
|
|
@@ -656,13 +657,17 @@ const MCP_WRITE_TOOLS = [
|
|
|
656
657
|
},
|
|
657
658
|
{
|
|
658
659
|
name: 'bclaw_release_claim',
|
|
659
|
-
description: 'Release a work claim.',
|
|
660
|
+
description: 'Release a work claim. Callers own their own claims; a trusted+ coordinator releasing another agent\'s claim MUST pass coordinator_override:true (audited).',
|
|
660
661
|
annotations: { tier: 'standard', category: 'coordination', headlessApproval: 'auto' },
|
|
661
662
|
inputSchema: {
|
|
662
663
|
type: 'object',
|
|
663
664
|
properties: {
|
|
664
665
|
id: { type: 'string', description: 'Claim ID to release.' },
|
|
665
666
|
planStatus: { type: 'string', description: 'Optional: update linked plan status.' },
|
|
667
|
+
coordinator_override: {
|
|
668
|
+
type: 'boolean',
|
|
669
|
+
description: 'Opt-in override for a trusted+ caller releasing a claim they do NOT own (cross-agent teardown, ghost-claim cleanup). Rejected for contributor-level callers; audited when used. trp#928.',
|
|
670
|
+
},
|
|
666
671
|
},
|
|
667
672
|
required: ['id'],
|
|
668
673
|
},
|
|
@@ -1188,7 +1193,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1188
1193
|
type: 'object',
|
|
1189
1194
|
properties: {
|
|
1190
1195
|
entity: { type: 'string', description: 'Entity name: plan | decision | constraint | trap | handoff | runtime_note | candidate | sequence | claim | action | assignment | agent_run | cross_project_link. Others not yet wired.' },
|
|
1191
|
-
filter: { type: 'object', description: 'Filter keys: status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6).
|
|
1196
|
+
filter: { type: 'object', description: 'Filter keys (ANY entity): status, tag (single tag), tags (array, any-match), author, plan_id, source, auto_generated, limit, offset, includeLegacy (bool, default false), minAutoReflectConfidence (0-1, default 0.6). ENTITY-SCOPED keys (rejected with a validation_error if used with any other entity): assignment_id, claim_id, message_id — ONLY for entity="agent_run". Unknown/mis-scoped keys are rejected loudly.' },
|
|
1192
1197
|
project: { type: 'string', description: 'Optional: name (or path/basename) of a linked project to query. Defaults to the current project. Only cross_project_links (config.yaml) and workspace store-chain children are accepted — list with `brainclaw link list`.' },
|
|
1193
1198
|
budget_tokens: { type: 'number', description: 'Optional token budget for the page payload (~4 chars/token). Tightens the default size cap; pagination metadata (has_more/next_offset) still applies.' },
|
|
1194
1199
|
},
|
|
@@ -1256,7 +1261,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1256
1261
|
},
|
|
1257
1262
|
{
|
|
1258
1263
|
name: 'bclaw_transition',
|
|
1259
|
-
description: 'Transition an entity to a new status. Validated against EntityRegistry.transitions. Returns the triggered side-effect tags. Pass `project` to transition an entity in a linked project instead of the current one.',
|
|
1264
|
+
description: 'Transition an entity to a new status. Validated against EntityRegistry.transitions. Returns the triggered side-effect tags. Pass `project` to transition an entity in a linked project instead of the current one. For entity="claim": released/stale transitions are ownership-checked — non-owners must pass coordinator_override:true (trusted+ trust level required).',
|
|
1260
1265
|
annotations: { tier: 'standard', category: 'memory', headlessApproval: 'prompt' },
|
|
1261
1266
|
inputSchema: {
|
|
1262
1267
|
type: 'object',
|
|
@@ -1266,6 +1271,7 @@ const MCP_WRITE_TOOLS = [
|
|
|
1266
1271
|
to: { type: 'string', description: 'Target status.' },
|
|
1267
1272
|
reason: { type: 'string', description: 'Optional free-text reason, audited alongside the transition.' },
|
|
1268
1273
|
project: { type: 'string', description: 'Optional: name of a linked project to transition the entity in. Defaults to the current project.' },
|
|
1274
|
+
coordinator_override: { type: 'boolean', description: 'entity="claim" only: opt-in override for a trusted+ caller releasing/staling a claim they do NOT own. Audited when used. trp#928.' },
|
|
1269
1275
|
},
|
|
1270
1276
|
required: ['entity', 'id', 'to'],
|
|
1271
1277
|
},
|
|
@@ -1829,10 +1835,22 @@ function ensureTrust(args, fields, level, cwd, sessionId) {
|
|
|
1829
1835
|
* missing field — which would then be silently GC'd by the state sync loop
|
|
1830
1836
|
* (see fix plan pln_5f44426c).
|
|
1831
1837
|
*
|
|
1832
|
-
* pln#562 step 3 —
|
|
1833
|
-
* author
|
|
1834
|
-
* invalid on read and silently GC'd
|
|
1835
|
-
*
|
|
1838
|
+
* pln#562 step 3 — a write that would create a record with a missing/'unknown'
|
|
1839
|
+
* author must never be silent (that produced records that passed creation but
|
|
1840
|
+
* were schema-invalid on read and silently GC'd from disk).
|
|
1841
|
+
*
|
|
1842
|
+
* pln#608 — extended with auto-repair: when the caller has no session but a
|
|
1843
|
+
* derivable agent name (arg / $BRAINCLAW_AGENT_NAME / detected AI agent),
|
|
1844
|
+
* fall through to `resolveOrAutoRegisterAgentIdentity` and materialize the
|
|
1845
|
+
* session via `buildOperationalIdentity({ persistImplicitSession: true })`
|
|
1846
|
+
* (same mechanic as switchProject:86-106 and session-start). The freshly-
|
|
1847
|
+
* created session is tagged `auto_created` so aggressive harvesting can
|
|
1848
|
+
* distinguish it from operator sessions (pln#602). The caller receives
|
|
1849
|
+
* `auto_repair` and surfaces it as a warning — never silent.
|
|
1850
|
+
*
|
|
1851
|
+
* KEEP (still a hard error, doctrine boundary): the identity is ambiguous
|
|
1852
|
+
* (no name in args, no env signal, no detectable agent). We do not invent
|
|
1853
|
+
* an identity — invoke intent is unclear and the write would misattribute.
|
|
1836
1854
|
*/
|
|
1837
1855
|
function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
1838
1856
|
const resolved = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
@@ -1842,9 +1860,92 @@ function resolveCanonicalAuthor(args, cwd, connectionSessionId) {
|
|
|
1842
1860
|
agent_id: resolved.identity.agent_id,
|
|
1843
1861
|
};
|
|
1844
1862
|
}
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1863
|
+
const strictError = 'error' in resolved && resolved.error ? resolved.error : undefined;
|
|
1864
|
+
// KEEP (doctrine boundary): a pinned principal that rejected the caller args
|
|
1865
|
+
// is a SPOOF/MISMATCH, not an ambiguous first-write. Never auto-repair over
|
|
1866
|
+
// it — silently re-attributing would defeat pln#562 step 3. The strict error
|
|
1867
|
+
// already carries the pointer to a curator override.
|
|
1868
|
+
if (resolveConnectionPrincipal(cwd, connectionSessionId)) {
|
|
1869
|
+
throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'principal mismatch'}`);
|
|
1870
|
+
}
|
|
1871
|
+
// Observer processes are read-only dashboards/inspectors. Even when an env
|
|
1872
|
+
// variable leaks an agent name into the observer process, canonical writes
|
|
1873
|
+
// must not use the auto-repair path because it can mint identity/session
|
|
1874
|
+
// state as a side effect.
|
|
1875
|
+
if (isObserverMode()) {
|
|
1876
|
+
throw new Error(`cannot resolve mutation author: ${strictError?.message ?? 'observer mode cannot auto-repair identity/session state'}`);
|
|
1877
|
+
}
|
|
1878
|
+
const explicitName = typeof args.agent === 'string' ? args.agent : undefined;
|
|
1879
|
+
const explicitId = typeof args.agentId === 'string' ? args.agentId : undefined;
|
|
1880
|
+
// resolveOrAutoRegisterAgentIdentity's fall-through helper only reads
|
|
1881
|
+
// BRAINCLAW_AGENT / OPENCLAW_AGENT. resolveCurrentAgentIdentity also honors
|
|
1882
|
+
// BRAINCLAW_AGENT_NAME, and dispatched workers set both. Normalize here so
|
|
1883
|
+
// an env-declared name is a first-class signal to the auto-repair path.
|
|
1884
|
+
const envAgentName = explicitName
|
|
1885
|
+
?? (process.env.BRAINCLAW_AGENT_NAME?.trim() || undefined)
|
|
1886
|
+
?? (process.env.BRAINCLAW_AGENT?.trim() || undefined);
|
|
1887
|
+
let identity;
|
|
1888
|
+
let autoRegistered;
|
|
1889
|
+
try {
|
|
1890
|
+
const outcome = resolveOrAutoRegisterAgentIdentity({
|
|
1891
|
+
agentName: envAgentName,
|
|
1892
|
+
agentId: explicitId,
|
|
1893
|
+
cwd,
|
|
1894
|
+
allowCurrent: true,
|
|
1895
|
+
allowEnv: true,
|
|
1896
|
+
});
|
|
1897
|
+
identity = outcome.identity;
|
|
1898
|
+
autoRegistered = outcome.auto_registered;
|
|
1899
|
+
}
|
|
1900
|
+
catch (err) {
|
|
1901
|
+
// Genuine ambiguity — no derivable name. Stays a hard error (KEEP: doctrine
|
|
1902
|
+
// boundary is "ambiguous intent → refuse with next_action", not silence).
|
|
1903
|
+
const detail = err instanceof Error ? err.message : (strictError?.message ?? String(err));
|
|
1904
|
+
throw new Error(`cannot resolve mutation author: ${detail} `
|
|
1905
|
+
+ 'Pass a registered agent, set $BRAINCLAW_AGENT_NAME, '
|
|
1906
|
+
+ 'or register with `brainclaw register-agent <name>` before writing.', { cause: err });
|
|
1907
|
+
}
|
|
1908
|
+
const explicitSessionId = connectionSessionId?.trim() || explicitSessionIdFromEnv();
|
|
1909
|
+
const hadSessionBefore = explicitSessionId
|
|
1910
|
+
? Boolean(loadSessionById(explicitSessionId, cwd))
|
|
1911
|
+
: Boolean(loadCurrentSession(cwd));
|
|
1912
|
+
let sessionAutoCreated;
|
|
1913
|
+
try {
|
|
1914
|
+
const opIdentity = buildOperationalIdentity(identity.agent_name, cwd, {
|
|
1915
|
+
agentId: identity.agent_id,
|
|
1916
|
+
sessionId: explicitSessionId,
|
|
1917
|
+
persistImplicitSession: true,
|
|
1918
|
+
});
|
|
1919
|
+
if (!hadSessionBefore && opIdentity.session_id) {
|
|
1920
|
+
sessionAutoCreated = opIdentity.session_id;
|
|
1921
|
+
const session = loadSessionById(opIdentity.session_id, cwd);
|
|
1922
|
+
if (session && !session.auto_created) {
|
|
1923
|
+
saveCurrentSession({ ...session, auto_created: true }, cwd);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
catch { /* best-effort — write can still proceed without a persisted session */ }
|
|
1928
|
+
const autoRepair = (autoRegistered || sessionAutoCreated)
|
|
1929
|
+
? {
|
|
1930
|
+
...(autoRegistered ? { agent_auto_registered: true } : {}),
|
|
1931
|
+
...(sessionAutoCreated ? { session_auto_created: sessionAutoCreated } : {}),
|
|
1932
|
+
}
|
|
1933
|
+
: undefined;
|
|
1934
|
+
return {
|
|
1935
|
+
agent_name: identity.agent_name,
|
|
1936
|
+
agent_id: identity.agent_id,
|
|
1937
|
+
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function renderAutoRepairWarning(auto_repair, agent_name) {
|
|
1941
|
+
const parts = [];
|
|
1942
|
+
if (auto_repair.agent_auto_registered) {
|
|
1943
|
+
parts.push(`agent '${agent_name}' auto-registered (first use). Run \`brainclaw register-agent ${agent_name}\` to set capabilities and trust level.`);
|
|
1944
|
+
}
|
|
1945
|
+
if (auto_repair.session_auto_created) {
|
|
1946
|
+
parts.push(`session ${auto_repair.session_auto_created} auto-created for this write.`);
|
|
1947
|
+
}
|
|
1948
|
+
return `⚠️ auto-repair: ${parts.join(' ')}`;
|
|
1848
1949
|
}
|
|
1849
1950
|
function explicitSessionIdFromEnv() {
|
|
1850
1951
|
return process.env.BRAINCLAW_SESSION_ID?.trim()
|
|
@@ -3561,18 +3662,43 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
3561
3662
|
catch {
|
|
3562
3663
|
return { response: createToolErrorResponse('not_found', `Claim not found: ${claimId}`) };
|
|
3563
3664
|
}
|
|
3564
|
-
// pln#562 step 5 — release is ownership-checked like acquisition
|
|
3565
|
-
// adoption.
|
|
3566
|
-
//
|
|
3665
|
+
// pln#562 step 5 + trp#928 — release is ownership-checked like acquisition
|
|
3666
|
+
// and adoption. Under the trp#928 tightening the coordinator override is
|
|
3667
|
+
// OPT-IN via coordinator_override:true (implicit "trusted+ = always
|
|
3668
|
+
// override" was too magic — a coordinator releasing a worker's claim
|
|
3669
|
+
// should be a visible act, not a silent side-effect of trust). The
|
|
3670
|
+
// ownership check still enforces:
|
|
3671
|
+
// - owner-of-claim releases (identity matches): allowed, no override needed
|
|
3672
|
+
// - non-owner releases without coordinator_override: rejected loudly (the
|
|
3673
|
+
// error message points the caller at coordinator_override so it is
|
|
3674
|
+
// executable — pln#607 rule).
|
|
3675
|
+
// - non-owner releases with coordinator_override:true but not trusted+:
|
|
3676
|
+
// trust_error (privilege escalation prevention).
|
|
3677
|
+
// - non-owner releases with coordinator_override:true and trusted+:
|
|
3678
|
+
// allowed, audited (auditReleaseOverride).
|
|
3567
3679
|
const releaseIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, cwd, connectionSessionId);
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3680
|
+
if ('error' in releaseIdentity && releaseIdentity.error) {
|
|
3681
|
+
const { kind, message, details } = releaseIdentity.error;
|
|
3682
|
+
return { response: createToolErrorResponse(kind, message, details) };
|
|
3683
|
+
}
|
|
3684
|
+
if (!('identity' in releaseIdentity) || !releaseIdentity.identity) {
|
|
3685
|
+
return { response: createToolErrorResponse('identity_error', 'No registered agent identity resolved for bclaw_release_claim.') };
|
|
3686
|
+
}
|
|
3687
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
3688
|
+
if (coordinatorOverrideRequested) {
|
|
3689
|
+
const trustLevel = releaseIdentity.identity.trust_level ?? 'contributor';
|
|
3690
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
3691
|
+
return {
|
|
3692
|
+
response: createToolErrorResponse('trust_error', `coordinator_override:true requires trust_level 'trusted' or higher — caller is '${trustLevel}'. Ask a curator to elevate the agent, or have the claim owner release it.`),
|
|
3693
|
+
};
|
|
3574
3694
|
}
|
|
3575
|
-
|
|
3695
|
+
}
|
|
3696
|
+
const releaseAuth = {
|
|
3697
|
+
agent: releaseIdentity.identity.agent_name,
|
|
3698
|
+
agent_id: releaseIdentity.identity.agent_id,
|
|
3699
|
+
session_id: connectionSessionId,
|
|
3700
|
+
override: coordinatorOverrideRequested,
|
|
3701
|
+
};
|
|
3576
3702
|
let cascadeResult;
|
|
3577
3703
|
try {
|
|
3578
3704
|
cascadeResult = releaseClaimWithCascade(claimId, {
|
|
@@ -4067,6 +4193,39 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
4067
4193
|
actor: callerAgent,
|
|
4068
4194
|
actor_id: resolved.identity.agent_id,
|
|
4069
4195
|
}, cwd);
|
|
4196
|
+
// trp#928 — cascade-release the assignment's linked claim on completion.
|
|
4197
|
+
// Before this landing an obedient worker had to make TWO calls to close
|
|
4198
|
+
// the loop (bclaw_assignment_update status=completed AND
|
|
4199
|
+
// bclaw_release_claim); dispatch briefs enumerate both, but not every
|
|
4200
|
+
// sandboxed worker gets through both, and the coordinator's harvest path
|
|
4201
|
+
// only releases on --integrate — so contributor-driven completions left
|
|
4202
|
+
// claims active. The worker's own identity owns the claim (session
|
|
4203
|
+
// adoption), so ownership matches and no coordinator_override is needed.
|
|
4204
|
+
// Silent success/failure is unacceptable: log per-claim outcome.
|
|
4205
|
+
if (status === 'completed' && assignment.claim_id) {
|
|
4206
|
+
try {
|
|
4207
|
+
const { releaseClaimsCascade, logCascadeReleaseResult } = await import('../core/claims.js');
|
|
4208
|
+
const cascade = releaseClaimsCascade([assignment.claim_id], {
|
|
4209
|
+
cwd,
|
|
4210
|
+
planStatus: 'done',
|
|
4211
|
+
auth: {
|
|
4212
|
+
agent: callerAgent,
|
|
4213
|
+
agent_id: resolved.identity.agent_id,
|
|
4214
|
+
session_id: effectiveSessionId,
|
|
4215
|
+
override: false,
|
|
4216
|
+
},
|
|
4217
|
+
});
|
|
4218
|
+
logCascadeReleaseResult({
|
|
4219
|
+
actor: callerAgent,
|
|
4220
|
+
trigger: 'assignment_completed',
|
|
4221
|
+
assignment_id: assignmentId,
|
|
4222
|
+
claim_id: assignment.claim_id,
|
|
4223
|
+
cascade,
|
|
4224
|
+
cwd,
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
catch { /* never block the update on cascade release */ }
|
|
4228
|
+
}
|
|
4070
4229
|
// When accepted: auto-acknowledge the inbox message (replaces bclaw_ack_message)
|
|
4071
4230
|
if (status === 'accepted' && assignment.message_id) {
|
|
4072
4231
|
try {
|
|
@@ -6515,17 +6674,38 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6515
6674
|
// only checks known keys), letting the caller believe the filter had
|
|
6516
6675
|
// applied when it hadn't. Under the new contract, an unknown key is
|
|
6517
6676
|
// a validation_error listing the keys actually honored.
|
|
6677
|
+
// trp#928 — the entity-scoping error is now first-class: the doc says
|
|
6678
|
+
// assignment_id/claim_id/message_id are entity='agent_run' only, but
|
|
6679
|
+
// before this the rejection message called them 'unknown', misleading
|
|
6680
|
+
// callers who'd cross-reference the description. Now the message names
|
|
6681
|
+
// the constraint AND the entity that DOES accept the key so the user
|
|
6682
|
+
// can fix the call without hunting through docs. (pln#599 docs-vs-facts.)
|
|
6518
6683
|
const KNOWN_FILTER_KEYS = new Set([
|
|
6519
6684
|
'status', 'tag', 'tags', 'author', 'plan_id', 'source', 'auto_generated',
|
|
6520
6685
|
'assignment_id', 'claim_id', 'message_id',
|
|
6521
6686
|
'limit', 'offset', 'includeLegacy', 'minAutoReflectConfidence',
|
|
6522
6687
|
]);
|
|
6523
6688
|
const agentRunOnlyFilterKeys = new Set(['assignment_id', 'claim_id', 'message_id']);
|
|
6524
|
-
const
|
|
6525
|
-
|
|
6689
|
+
const providedKeys = Object.keys(filter);
|
|
6690
|
+
const unknownKeys = providedKeys.filter((k) => !KNOWN_FILTER_KEYS.has(k));
|
|
6691
|
+
const misScopedKeys = providedKeys.filter((k) => agentRunOnlyFilterKeys.has(k) && entity !== 'agent_run');
|
|
6692
|
+
if (unknownKeys.length > 0 || misScopedKeys.length > 0) {
|
|
6693
|
+
const parts = [];
|
|
6694
|
+
if (unknownKeys.length > 0) {
|
|
6695
|
+
parts.push(`Unknown filter key(s): ${unknownKeys.map((k) => `"${k}"`).join(', ')}. Accepted keys: ${[...KNOWN_FILTER_KEYS].sort().join(', ')}.`);
|
|
6696
|
+
}
|
|
6697
|
+
if (misScopedKeys.length > 0) {
|
|
6698
|
+
parts.push(`Filter key(s) ${misScopedKeys.map((k) => `"${k}"`).join(', ')} are only valid for entity="agent_run" `
|
|
6699
|
+
+ `(this call used entity="${entity}"). `
|
|
6700
|
+
+ `Retry with entity="agent_run", or drop the ${misScopedKeys.join('/')} filter.`);
|
|
6701
|
+
}
|
|
6526
6702
|
return {
|
|
6527
|
-
response: createToolErrorResponse('validation_error',
|
|
6528
|
-
|
|
6703
|
+
response: createToolErrorResponse('validation_error', parts.join(' '), {
|
|
6704
|
+
unknown_keys: unknownKeys,
|
|
6705
|
+
mis_scoped_keys: misScopedKeys,
|
|
6706
|
+
accepted_keys: [...KNOWN_FILTER_KEYS].sort(),
|
|
6707
|
+
agent_run_only_keys: [...agentRunOnlyFilterKeys],
|
|
6708
|
+
}),
|
|
6529
6709
|
};
|
|
6530
6710
|
}
|
|
6531
6711
|
const result = listEntities(entity, targetCwd, filter);
|
|
@@ -6664,29 +6844,36 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6664
6844
|
const data = { ...rawData };
|
|
6665
6845
|
let actor = typeof data.author === 'string' ? data.author : undefined;
|
|
6666
6846
|
let actorId = typeof data.agent_id === 'string' ? data.agent_id : undefined;
|
|
6847
|
+
let autoRepair;
|
|
6667
6848
|
if (data.author === undefined) {
|
|
6668
|
-
const
|
|
6669
|
-
data.author = agent_name;
|
|
6849
|
+
const author = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6850
|
+
data.author = author.agent_name;
|
|
6670
6851
|
if (data.agent === undefined)
|
|
6671
|
-
data.agent = agent_name;
|
|
6672
|
-
if (data.agent_id === undefined && agent_id)
|
|
6673
|
-
data.agent_id = agent_id;
|
|
6674
|
-
actor = agent_name;
|
|
6675
|
-
actorId = agent_id;
|
|
6852
|
+
data.agent = author.agent_name;
|
|
6853
|
+
if (data.agent_id === undefined && author.agent_id)
|
|
6854
|
+
data.agent_id = author.agent_id;
|
|
6855
|
+
actor = author.agent_name;
|
|
6856
|
+
actorId = author.agent_id;
|
|
6857
|
+
autoRepair = author.auto_repair;
|
|
6676
6858
|
}
|
|
6677
6859
|
else if (data.agent === undefined) {
|
|
6678
6860
|
data.agent = data.author;
|
|
6679
6861
|
}
|
|
6680
6862
|
const result = createEntity(entity, data, targetCwd);
|
|
6681
6863
|
appendAuditEntry({ actor: actor ?? 'unknown', ...(actorId ? { actor_id: actorId } : {}), action: 'create', item_id: result.id, item_type: entity }, targetCwd);
|
|
6864
|
+
const createText = `✔ created ${entity} ${result.id}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
6865
|
+
const createContent = autoRepair
|
|
6866
|
+
? [{ type: 'text', text: createText }, { type: 'text', text: renderAutoRepairWarning(autoRepair, actor ?? 'unknown') }]
|
|
6867
|
+
: [{ type: 'text', text: createText }];
|
|
6682
6868
|
return {
|
|
6683
6869
|
response: toolResponse({
|
|
6684
|
-
content:
|
|
6870
|
+
content: createContent,
|
|
6685
6871
|
structuredContent: {
|
|
6686
6872
|
...result,
|
|
6687
6873
|
resolved_project: targetScope.resolved_project,
|
|
6688
6874
|
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
6689
6875
|
...(autoSwitched ? { auto_switched: true } : {}),
|
|
6876
|
+
...(autoRepair ? { auto_repair: autoRepair } : {}),
|
|
6690
6877
|
},
|
|
6691
6878
|
}),
|
|
6692
6879
|
};
|
|
@@ -6702,16 +6889,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6702
6889
|
const patch = (args.patch ?? {});
|
|
6703
6890
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6704
6891
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6705
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6892
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6706
6893
|
const result = updateEntity(entity, id, patch, targetCwd);
|
|
6707
6894
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity }, targetCwd);
|
|
6895
|
+
const updateText = `✔ updated ${entity} ${id}`;
|
|
6896
|
+
const updateContent = auto_repair
|
|
6897
|
+
? [{ type: 'text', text: updateText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6898
|
+
: [{ type: 'text', text: updateText }];
|
|
6708
6899
|
return {
|
|
6709
6900
|
response: toolResponse({
|
|
6710
|
-
content:
|
|
6901
|
+
content: updateContent,
|
|
6711
6902
|
structuredContent: {
|
|
6712
6903
|
...result,
|
|
6713
6904
|
resolved_project: targetScope.resolved_project,
|
|
6714
6905
|
active_source: targetScope.active_source,
|
|
6906
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6715
6907
|
},
|
|
6716
6908
|
}),
|
|
6717
6909
|
};
|
|
@@ -6727,16 +6919,21 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6727
6919
|
const purge = args.purge === true;
|
|
6728
6920
|
const targetCwd = resolveProjectCwd(args.project, cwd);
|
|
6729
6921
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6730
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6922
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6731
6923
|
const result = removeEntity(entity, id, targetCwd, purge);
|
|
6732
6924
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'delete', item_id: id, item_type: entity, reason: purge ? 'purged' : 'archived' }, targetCwd);
|
|
6925
|
+
const removeText = `✔ removed ${entity} ${id}`;
|
|
6926
|
+
const removeContent = auto_repair
|
|
6927
|
+
? [{ type: 'text', text: removeText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6928
|
+
: [{ type: 'text', text: removeText }];
|
|
6733
6929
|
return {
|
|
6734
6930
|
response: toolResponse({
|
|
6735
|
-
content:
|
|
6931
|
+
content: removeContent,
|
|
6736
6932
|
structuredContent: {
|
|
6737
6933
|
...result,
|
|
6738
6934
|
resolved_project: targetScope.resolved_project,
|
|
6739
6935
|
active_source: targetScope.active_source,
|
|
6936
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6740
6937
|
},
|
|
6741
6938
|
}),
|
|
6742
6939
|
};
|
|
@@ -6752,13 +6949,20 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6752
6949
|
const toProject = String(args.to_project ?? '');
|
|
6753
6950
|
const fromProject = typeof args.from_project === 'string' ? args.from_project : undefined;
|
|
6754
6951
|
const force = args.force === true;
|
|
6755
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6952
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6756
6953
|
const result = relocateEntity({ entity, id, toProject, fromProject, force, cwd, actor: agent_name, actorId: agent_id });
|
|
6757
6954
|
const warn = result.warnings.length ? ` (${result.warnings.length} warning(s))` : '';
|
|
6955
|
+
const moveText = `✔ moved ${entity} ${id} → ${result.to}${warn}`;
|
|
6956
|
+
const moveContent = auto_repair
|
|
6957
|
+
? [{ type: 'text', text: moveText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
6958
|
+
: [{ type: 'text', text: moveText }];
|
|
6758
6959
|
return {
|
|
6759
6960
|
response: toolResponse({
|
|
6760
|
-
content:
|
|
6761
|
-
structuredContent: {
|
|
6961
|
+
content: moveContent,
|
|
6962
|
+
structuredContent: {
|
|
6963
|
+
...result,
|
|
6964
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6965
|
+
},
|
|
6762
6966
|
}),
|
|
6763
6967
|
};
|
|
6764
6968
|
}
|
|
@@ -6788,17 +6992,48 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
6788
6992
|
const to = String(args.to ?? '');
|
|
6789
6993
|
const reason = args.reason;
|
|
6790
6994
|
const targetScope = scopeMetadataForTarget(args, targetCwd, scopeInfo);
|
|
6791
|
-
const { agent_name, agent_id } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6792
|
-
|
|
6995
|
+
const { agent_name, agent_id, auto_repair } = resolveCanonicalAuthor(args, cwd, connectionSessionId);
|
|
6996
|
+
// trp#928 — claim transitions consume the ReleaseClaimAuth ownership
|
|
6997
|
+
// check (released/stale both mutate a claim owned by SOME agent). Reuse
|
|
6998
|
+
// the same coordinator_override opt-in as bclaw_release_claim so both
|
|
6999
|
+
// paths have identical trust semantics and the same executable error.
|
|
7000
|
+
let transitionAuth;
|
|
7001
|
+
if (entity === 'claim') {
|
|
7002
|
+
const transitionIdentity = resolveMutationIdentity(args, { nameField: 'agent', idField: 'agentId' }, targetCwd, connectionSessionId);
|
|
7003
|
+
const coordinatorOverrideRequested = args.coordinator_override === true;
|
|
7004
|
+
if (coordinatorOverrideRequested) {
|
|
7005
|
+
const identity = 'identity' in transitionIdentity ? transitionIdentity.identity : undefined;
|
|
7006
|
+
const trustLevel = identity?.trust_level ?? 'contributor';
|
|
7007
|
+
if (!hasMinimumTrustLevel(trustLevel, 'trusted')) {
|
|
7008
|
+
return {
|
|
7009
|
+
response: createToolErrorResponse('trust_error', `coordinator_override:true requires trust_level 'trusted' or higher — caller is '${trustLevel}'.`),
|
|
7010
|
+
};
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
transitionAuth = 'identity' in transitionIdentity && transitionIdentity.identity
|
|
7014
|
+
? {
|
|
7015
|
+
agent: transitionIdentity.identity.agent_name,
|
|
7016
|
+
agent_id: transitionIdentity.identity.agent_id,
|
|
7017
|
+
session_id: connectionSessionId,
|
|
7018
|
+
override: coordinatorOverrideRequested,
|
|
7019
|
+
}
|
|
7020
|
+
: undefined;
|
|
7021
|
+
}
|
|
7022
|
+
const result = transitionEntity(entity, id, to, targetCwd, reason, transitionAuth);
|
|
6793
7023
|
appendAuditEntry({ actor: agent_name, ...(agent_id ? { actor_id: agent_id } : {}), action: 'update', item_id: id, item_type: entity, reason: `transition ${result.from} → ${to}${reason ? ` (${reason})` : ''}` }, targetCwd);
|
|
7024
|
+
const transitionText = `✔ ${entity} ${id}: ${result.from} → ${to}${autoSwitched ? ` (auto-switched → ${targetScope.resolved_project.name ?? targetScope.resolved_project.path})` : ''}`;
|
|
7025
|
+
const transitionContent = auto_repair
|
|
7026
|
+
? [{ type: 'text', text: transitionText }, { type: 'text', text: renderAutoRepairWarning(auto_repair, agent_name) }]
|
|
7027
|
+
: [{ type: 'text', text: transitionText }];
|
|
6794
7028
|
return {
|
|
6795
7029
|
response: toolResponse({
|
|
6796
|
-
content:
|
|
7030
|
+
content: transitionContent,
|
|
6797
7031
|
structuredContent: {
|
|
6798
7032
|
...result,
|
|
6799
7033
|
resolved_project: targetScope.resolved_project,
|
|
6800
7034
|
active_source: autoSwitched ? 'auto_switch' : targetScope.active_source,
|
|
6801
7035
|
...(autoSwitched ? { auto_switched: true } : {}),
|
|
7036
|
+
...(auto_repair ? { auto_repair } : {}),
|
|
6802
7037
|
},
|
|
6803
7038
|
}),
|
|
6804
7039
|
};
|
|
@@ -3,16 +3,36 @@ import { mutate } from '../core/mutation-pipeline.js';
|
|
|
3
3
|
import { loadClaim, listClaims, releaseClaim } from '../core/claims.js';
|
|
4
4
|
import { rebuildProjectMd } from '../core/markdown.js';
|
|
5
5
|
import { loadState, mutateState } from '../core/state.js';
|
|
6
|
+
import { requireMinimumTrustLevel, requireRegisteredAgentIdentity } from '../core/agent-registry.js';
|
|
6
7
|
export function runReleaseClaim(id, options = {}) {
|
|
7
8
|
if (!memoryExists(options.cwd)) {
|
|
8
9
|
console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
13
|
+
// Surface split (trp#928 follow-up): the ownership gate lives on the MCP
|
|
14
|
+
// surface (bclaw_release_claim / bclaw_transition), where agent callers
|
|
15
|
+
// carry a session-bound identity. The CLI `release-claim <id>` is the
|
|
16
|
+
// operator/scripting surface and keeps its historic unguarded semantics —
|
|
17
|
+
// the e2e contract (collaboration.test.ts) has always released cross-agent
|
|
18
|
+
// claims from an env-identified CLI. Deriving an ambient identity here and
|
|
19
|
+
// gating on it turned every operator release into a false ownership
|
|
20
|
+
// mismatch (silent-default anti-pattern, pln#607). `--coordinator-override`
|
|
21
|
+
// stays available to make a cross-agent release explicit and audited.
|
|
22
|
+
let releaseAuth;
|
|
23
|
+
if (options.coordinatorOverride) {
|
|
24
|
+
const identity = requireRegisteredAgentIdentity({ cwd: options.cwd, allowCurrent: true, allowEnv: true });
|
|
25
|
+
requireMinimumTrustLevel(identity, 'trusted');
|
|
26
|
+
releaseAuth = {
|
|
27
|
+
agent: identity.agent_name,
|
|
28
|
+
agent_id: identity.agent_id,
|
|
29
|
+
override: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
12
32
|
let claim = loadClaim(id, options.cwd);
|
|
13
33
|
mutate({ cwd: options.cwd }, () => {
|
|
14
34
|
const existing = loadClaim(id, options.cwd);
|
|
15
|
-
claim = releaseClaim(id, options.cwd);
|
|
35
|
+
claim = releaseClaim(id, options.cwd, releaseAuth);
|
|
16
36
|
if (existing.plan_id) {
|
|
17
37
|
const updated = mutateState((state) => {
|
|
18
38
|
const plan = state.plan_items.find((item) => item.id === existing.plan_id);
|
|
@@ -175,6 +175,11 @@ const PROFILES = {
|
|
|
175
175
|
// Aligning with the regular spawn template (workspace-write) is the
|
|
176
176
|
// accepted pattern per agent_spawn_inventory memory.
|
|
177
177
|
invoke_review_template: 'codex exec -c approval_policy="never" --sandbox workspace-write "{prompt}"',
|
|
178
|
+
// pln#606: `codex exec -m <MODEL>` / `--model` (verified empirically on
|
|
179
|
+
// codex 0.130). We use the long form `--model` for symmetry with the
|
|
180
|
+
// other agent profiles and readability.
|
|
181
|
+
model_flag: '--model',
|
|
182
|
+
model_flag_insert_index: 2,
|
|
178
183
|
},
|
|
179
184
|
antigravity: {
|
|
180
185
|
name: 'antigravity', category: 'code-agent', workflowModel: 'interactive',
|
|
@@ -204,6 +209,10 @@ const PROFILES = {
|
|
|
204
209
|
invoke_template: 'copilot -p "{prompt}" --allow-all --no-ask-user',
|
|
205
210
|
invoke_binary: 'copilot',
|
|
206
211
|
invoke_review_template: 'copilot -p "{prompt}" --allow-all --no-ask-user',
|
|
212
|
+
// pln#606: `copilot --model <model>` (verified on Copilot CLI 1.0.35+).
|
|
213
|
+
// 'auto' lets Copilot pick automatically; concrete ids come from the
|
|
214
|
+
// entitled catalog fetched by the CLI at startup.
|
|
215
|
+
model_flag: '--model',
|
|
207
216
|
},
|
|
208
217
|
kilocode: {
|
|
209
218
|
name: 'kilocode', category: 'code-agent', workflowModel: 'interactive',
|
|
@@ -564,11 +573,13 @@ export function buildInvokeCommand(name, prompt, options = {}) {
|
|
|
564
573
|
const rawTokens = parseTemplateString(templateStr);
|
|
565
574
|
if (rawTokens.length === 0)
|
|
566
575
|
return undefined;
|
|
567
|
-
// pln#520 step 3: inject the resolved model
|
|
568
|
-
// choice is decoupled from agent identity. Only when the
|
|
569
|
-
// `model_flag` and the template doesn't already pin a model
|
|
576
|
+
// pln#520 step 3: inject the resolved model at the profile's model argument
|
|
577
|
+
// position so model choice is decoupled from agent identity. Only when the
|
|
578
|
+
// profile declares a `model_flag` and the template doesn't already pin a model
|
|
579
|
+
// (don't double it).
|
|
570
580
|
if (options.model && profile.model_flag && !rawTokens.includes(profile.model_flag)) {
|
|
571
|
-
|
|
581
|
+
const insertIndex = Math.min(Math.max(profile.model_flag_insert_index ?? 1, 1), rawTokens.length);
|
|
582
|
+
rawTokens.splice(insertIndex, 0, profile.model_flag, options.model);
|
|
572
583
|
}
|
|
573
584
|
const executable = rawTokens[0];
|
|
574
585
|
const interpolatedTokens = rawTokens.slice(1).map((tok) => tok === '{prompt}' ? embeddedPrompt : tok);
|
|
@@ -150,7 +150,13 @@ function buildIdentityKey(agentId, env = process.env, forceRegenerate = false) {
|
|
|
150
150
|
let publicKeyPem;
|
|
151
151
|
if (!forceRegenerate && fs.existsSync(filepath)) {
|
|
152
152
|
const privateKey = crypto.createPrivateKey(fs.readFileSync(filepath, 'utf-8'));
|
|
153
|
-
|
|
153
|
+
// @types/node 26 dropped the KeyObject overload from createPublicKey's signature
|
|
154
|
+
// (regression — Node accepts a private KeyObject to derive its public key, as documented).
|
|
155
|
+
// Cast to a parameter type the .d.ts still accepts; runtime behaviour is unchanged.
|
|
156
|
+
publicKeyPem = crypto
|
|
157
|
+
.createPublicKey(privateKey)
|
|
158
|
+
.export({ type: 'spki', format: 'pem' })
|
|
159
|
+
.toString();
|
|
154
160
|
}
|
|
155
161
|
else {
|
|
156
162
|
const generated = crypto.generateKeyPairSync('ed25519');
|