@smartmemory/compose 0.2.5-beta → 0.2.7-beta
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/bin/compose.js +7 -0
- package/bin/git-hooks/pre-push.template +41 -13
- package/dist/assets/{App-j8fWZcGr.js → App-D3ehVPvi.js} +4 -4
- package/dist/assets/{arc-BFqOo_jJ.js → arc-Dmf69iHG.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-D722w0RE.js → architectureDiagram-3BPJPVTR-xYo993Yw.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-B4w0mOAJ.js → blockDiagram-GPEHLZMM-UX4EF98O.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-D6LE8-j8.js → c4Diagram-AAUBKEIU-DaP9CGWb.js} +1 -1
- package/dist/assets/channel-D_RXsFFT.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-CrazA7xu.js → chunk-2J33WTMH-CKk_RN3A.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-Cp90GiCM.js → chunk-4BX2VUAB-DboAwYKw.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bnais1SK.js → chunk-55IACEB6-Dsy9RYvI.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-kD07Sqp5.js → chunk-727SXJPM-fAH0QO9v.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-DmIxhJc8.js → chunk-AQP2D5EJ-DyZYerFP.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Jti_und8.js → chunk-FMBD7UC4-BnboGO5t.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-Ipx3noKz.js → chunk-ND2GUHAM-Di9tYXme.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CeblRnPF.js → chunk-QZHKN3VN-zRPRlAIL.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-K6wdB4ic.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-K6wdB4ic.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-fNQlSmHt.js → cose-bilkent-S5V4N54A-C7Hqukaf.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-D27D6YAL.js → dagre-BM42HDAG-B-cR-BjI.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-CtXeohzN.js → diagram-2AECGRRQ-B6-5onDk.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-C_BqZkx0.js → diagram-5GNKFQAL-DoZZgFAM.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-B29ynQz4.js → diagram-KO2AKTUF-77jEGlJh.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-DAYJMc2I.js → diagram-LMA3HP47-D3S7XDRD.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-CBJMis3l.js → diagram-OG6HWLK6-KbYL9aCY.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-nd3GWiPn.js → erDiagram-TEJ5UH35-DezFbJP-.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-HFUno_nV.js → flowDiagram-I6XJVG4X-4x31cK9j.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-CPPAAjwR.js → ganttDiagram-6RSMTGT7-FopfSTyZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-NBq1F6K2.js → gitGraphDiagram-PVQCEYII-DSiQGKbN.js} +1 -1
- package/dist/assets/graph-Cs_vqCR0.js +331 -0
- package/dist/assets/{index-uHKnp74B.js → index-ClX6LVAf.js} +2 -2
- package/dist/assets/{infoDiagram-5YYISTIA-D-TOBtCq.js → infoDiagram-5YYISTIA-DE6BqzK_.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-nXOztZiZ.js → ishikawaDiagram-YF4QCWOH-Dml8NwQI.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-Bko3tTdh.js → journeyDiagram-JHISSGLW-CwWeJgjE.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-1e-7i8st.js → kanban-definition-UN3LZRKU-DnG956Wh.js} +1 -1
- package/dist/assets/{linear-Dx5ZJB7F.js → linear-CA3N7Rpi.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-CNwNkDqN.js → mindmap-definition-RKZ34NQL-CxfIOjLX.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-C5fvCej-.js → pieDiagram-4H26LBE5-O7aIwy1x.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-4NoQsF61.js → quadrantDiagram-W4KKPZXB-CPQ2qq7c.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-q5WxB9LO.js → requirementDiagram-4Y6WPE33-C23horL4.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-DlQNB367.js → sankeyDiagram-5OEKKPKP-DPY04kOW.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-BzHclOKt.js → sequenceDiagram-3UESZ5HK-BKaTfIvo.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-BvWRI9zK.js → stateDiagram-AJRCARHV-B9na_6mY.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-Cf84VDiH.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-j2wKjAti.js → timeline-definition-PNZ67QCA-BBWPqd7X.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-B77g7htC.js → vennDiagram-CIIHVFJN-tWqiHsOZ.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-83Im2mo2.js → wardley-L42UT6IY-DorxG6os.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-CK-XB-bO.js → wardleyDiagram-YWT4CUSO-B49f8GzW.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-D42FcVOY.js → xychartDiagram-2RQKCTM6-BgKSj8Qb.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build.js +4 -1
- package/lib/result-normalizer.js +5 -1
- package/package.json +2 -2
- package/server/agent-spawn.js +7 -1
- package/server/compose-mcp-tools.js +103 -1
- package/server/compose-mcp.js +34 -4
- package/server/design-routes.js +4 -1
- package/server/mcp-tool-policy.js +112 -0
- package/dist/assets/channel-BD-5_hPW.js +0 -1
- package/dist/assets/classDiagram-4FO5ZUOK-mSW5R7DY.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-mSW5R7DY.js +0 -1
- package/dist/assets/graph-CJVNlri5.js +0 -331
- package/dist/assets/stateDiagram-v2-BHNVJYJU-CDlF0VA8.js +0 -1
package/server/agent-spawn.js
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path';
|
|
|
9
9
|
|
|
10
10
|
import { getTargetRoot } from './project-root.js';
|
|
11
11
|
import { gracefulKill } from './agent-health.js';
|
|
12
|
+
import { resolveSpawnProfile } from './mcp-tool-policy.js';
|
|
12
13
|
|
|
13
14
|
const PROJECT_ROOT = getTargetRoot();
|
|
14
15
|
|
|
@@ -38,7 +39,7 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
|
|
|
38
39
|
const _agents = new Map();
|
|
39
40
|
// POST /api/agent/spawn — spawn a hidden Claude subagent
|
|
40
41
|
app.post('/api/agent/spawn', requireSensitiveToken, (req, res) => {
|
|
41
|
-
const { prompt, id } = req.body || {};
|
|
42
|
+
const { prompt, id, profile, template } = req.body || {};
|
|
42
43
|
if (!prompt || typeof prompt !== 'string') return res.status(400).json({ error: 'prompt is required and must be a string' });
|
|
43
44
|
|
|
44
45
|
const agentId = id || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -49,6 +50,11 @@ export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadc
|
|
|
49
50
|
|
|
50
51
|
const cleanEnv = { ...process.env, NO_COLOR: '1' };
|
|
51
52
|
delete cleanEnv.CLAUDECODE;
|
|
53
|
+
// COMP-MCP-ENFORCE-1: inject a TRUSTED, spawn-time MCP profile the subagent
|
|
54
|
+
// cannot rewrite (its compose-mcp child inherits this env). Only restrictive
|
|
55
|
+
// profiles are injected; an orchestrator/unspecified spawn stays unrestricted.
|
|
56
|
+
const spawnProfile = resolveSpawnProfile({ profile, template });
|
|
57
|
+
if (spawnProfile) cleanEnv.COMPOSE_SESSION_PROFILE = spawnProfile;
|
|
52
58
|
|
|
53
59
|
const proc = spawn('claude', [
|
|
54
60
|
'-p', prompt,
|
|
@@ -10,6 +10,7 @@ import http from 'node:http';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
|
|
12
12
|
import { getTargetRoot, getDataDir, resolveProjectPath, switchProject, setCurrentWorkspaceId, loadProjectConfig } from './project-root.js';
|
|
13
|
+
import { resolveProfile, isToolAllowed } from './mcp-tool-policy.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* COMP-MCP-ENFORCE Slice 3 — kill the `force` escape hatch at the MCP tool
|
|
@@ -438,7 +439,7 @@ export async function toolValidateProject(args = {}) {
|
|
|
438
439
|
});
|
|
439
440
|
}
|
|
440
441
|
|
|
441
|
-
export async function toolBindSession({ featureCode }) {
|
|
442
|
+
export async function toolBindSession({ featureCode, profile } = {}) {
|
|
442
443
|
let result;
|
|
443
444
|
try {
|
|
444
445
|
result = await _httpRequest('POST', '/api/session/bind', { featureCode });
|
|
@@ -452,6 +453,15 @@ export async function toolBindSession({ featureCode }) {
|
|
|
452
453
|
: `HTTP ${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`;
|
|
453
454
|
throw new Error(errMsg);
|
|
454
455
|
}
|
|
456
|
+
// COMP-MCP-ENFORCE-1: record the bound feature (anchor for phase resolution)
|
|
457
|
+
// on any non-error reply — including `already_bound` — so reconnects/repeat
|
|
458
|
+
// binds keep the anchor. Use the server's AUTHORITATIVE featureCode from the
|
|
459
|
+
// reply (an `already_bound` reply returns the real bound feature, which may
|
|
460
|
+
// differ from the request arg) so the anchor never drifts. The trusted env
|
|
461
|
+
// profile is the floor; the bind `profile` arg may only NARROW it.
|
|
462
|
+
const boundCode = (body && typeof body === 'object' && body.featureCode) || featureCode;
|
|
463
|
+
if (boundCode) _boundFeatureCode = boundCode;
|
|
464
|
+
_sessionProfile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, profile);
|
|
455
465
|
return body;
|
|
456
466
|
}
|
|
457
467
|
|
|
@@ -627,3 +637,95 @@ export function toolGetWorkspace() {
|
|
|
627
637
|
|
|
628
638
|
export function _getBinding() { return _binding?.id ?? null; }
|
|
629
639
|
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// COMP-MCP-ENFORCE-1 — phase-scoped MCP tool gate (profile × phase)
|
|
642
|
+
//
|
|
643
|
+
// Trusted profile = spawn-injected COMPOSE_SESSION_PROFILE (the agent cannot
|
|
644
|
+
// rewrite its own launch env); bind_session may only NARROW it. The bound
|
|
645
|
+
// feature anchor (_boundFeatureCode) lets the gate resolve the current phase
|
|
646
|
+
// on-disk and check that re-permitted mutations target the bound feature.
|
|
647
|
+
// All process-global by intent (one MCP child per session).
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
let _sessionProfile = resolveProfile(process.env.COMPOSE_SESSION_PROFILE, null);
|
|
651
|
+
let _boundFeatureCode = null;
|
|
652
|
+
|
|
653
|
+
export function _getSessionProfile() { return _sessionProfile; }
|
|
654
|
+
export function _getBoundFeatureCode() { return _boundFeatureCode; }
|
|
655
|
+
/** @internal test seam */
|
|
656
|
+
export function _testOnly_setSessionContext({ profile, boundFeatureCode } = {}) {
|
|
657
|
+
if (profile !== undefined) _sessionProfile = profile;
|
|
658
|
+
if (boundFeatureCode !== undefined) _boundFeatureCode = boundFeatureCode;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** The bound feature's current lifecycle phase from vision-state.json, or null. */
|
|
662
|
+
export function resolveBoundPhase() {
|
|
663
|
+
if (!_boundFeatureCode) return null;
|
|
664
|
+
try {
|
|
665
|
+
const { items } = loadVisionState();
|
|
666
|
+
const item = items.find((i) => i.lifecycle?.featureCode === _boundFeatureCode);
|
|
667
|
+
return item?.lifecycle?.currentPhase ?? null;
|
|
668
|
+
} catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Resolve a gated tool's target to a feature code (for the feature-scoped re-permit check). */
|
|
674
|
+
function _resolveTargetFeatureCode(tool, args = {}) {
|
|
675
|
+
try {
|
|
676
|
+
if (tool === 'record_completion') return args.feature_code ?? null;
|
|
677
|
+
if (tool === 'set_feature_status' || tool === 'add_roadmap_entry' || tool === 'propose_followup') {
|
|
678
|
+
return args.code ?? null;
|
|
679
|
+
}
|
|
680
|
+
if (tool === 'complete_feature' || tool === 'kill_feature') {
|
|
681
|
+
const { items } = loadVisionState();
|
|
682
|
+
return items.find((i) => i.id === args.id)?.lifecycle?.featureCode ?? null;
|
|
683
|
+
}
|
|
684
|
+
if (tool === 'approve_gate') {
|
|
685
|
+
const { items, gates } = loadVisionState();
|
|
686
|
+
const gate = gates?.find((g) => g.id === args.gateId);
|
|
687
|
+
return items.find((i) => i.id === gate?.itemId)?.lifecycle?.featureCode ?? null;
|
|
688
|
+
}
|
|
689
|
+
} catch { /* fall through */ }
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function _targetMatchesBoundFeature(tool, args) {
|
|
694
|
+
if (!_boundFeatureCode) return false;
|
|
695
|
+
const code = _resolveTargetFeatureCode(tool, args);
|
|
696
|
+
return code !== null && code === _boundFeatureCode;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Throw PHASE_TOOL_DENIED if the tool is not allowed for the current
|
|
701
|
+
* profile×phase. No-op when the capability is off (default) or on a valid
|
|
702
|
+
* override token. On unresolved CONTEXT the behavior is graduated, NOT blanket
|
|
703
|
+
* fail-open: an unresolved PROFILE (no/unknown env) normalizes to orchestrator →
|
|
704
|
+
* unrestricted; an unresolved PHASE only fails open the phase *refinement* — the
|
|
705
|
+
* profile BASE policy (implementer deny / reviewer allowlist) still applies
|
|
706
|
+
* (a restricted context with unknown phase stays restricted, which is the safe
|
|
707
|
+
* reading). `_testCtx` lets tests drive flag/profile/phase/target without disk/env.
|
|
708
|
+
*/
|
|
709
|
+
export function assertToolPhaseAllowed(tool, args = {}, _testCtx) {
|
|
710
|
+
const guardOn = _testCtx?.phaseScopedTools ?? (loadProjectConfig()?.capabilities?.phaseScopedTools === true);
|
|
711
|
+
if (!guardOn) return;
|
|
712
|
+
if (_overrideOk(args)) return;
|
|
713
|
+
|
|
714
|
+
const profile = _testCtx?.profile ?? _sessionProfile;
|
|
715
|
+
const phase = _testCtx?.phase ?? resolveBoundPhase();
|
|
716
|
+
const targetMatchesBoundFeature = _testCtx?.targetMatches ?? _targetMatchesBoundFeature(tool, args);
|
|
717
|
+
|
|
718
|
+
const verdict = isToolAllowed({ tool, profile, phase, targetMatchesBoundFeature });
|
|
719
|
+
if (!verdict.allowed) {
|
|
720
|
+
const e = new Error(
|
|
721
|
+
`${tool} is not available to profile '${profile}'` +
|
|
722
|
+
(phase ? ` in phase '${phase}'` : '') + `: ${verdict.reason}. ` +
|
|
723
|
+
`Supply a valid override_token to deviate.`,
|
|
724
|
+
);
|
|
725
|
+
e.code = 'PHASE_TOOL_DENIED';
|
|
726
|
+
e.profile = profile;
|
|
727
|
+
e.phase = phase;
|
|
728
|
+
throw e;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
package/server/compose-mcp.js
CHANGED
|
@@ -64,8 +64,12 @@ import {
|
|
|
64
64
|
toolWriteCheckpoint,
|
|
65
65
|
toolComposeResume,
|
|
66
66
|
_getBinding,
|
|
67
|
+
assertToolPhaseAllowed,
|
|
68
|
+
_getSessionProfile,
|
|
69
|
+
resolveBoundPhase,
|
|
67
70
|
} from './compose-mcp-tools.js';
|
|
68
|
-
import {
|
|
71
|
+
import { isToolAllowed } from './mcp-tool-policy.js';
|
|
72
|
+
import { switchProject, getTargetRoot, loadProjectConfig } from './project-root.js';
|
|
69
73
|
import { resolveWorkspace } from '../lib/resolve-workspace.js';
|
|
70
74
|
|
|
71
75
|
// ---------------------------------------------------------------------------
|
|
@@ -627,15 +631,41 @@ const server = new Server(
|
|
|
627
631
|
{ capabilities: { tools: {} } }
|
|
628
632
|
);
|
|
629
633
|
|
|
630
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
631
|
-
|
|
632
|
-
|
|
634
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
635
|
+
// COMP-MCP-ENFORCE-1: best-effort surface filter by the session's profile×phase.
|
|
636
|
+
// The hard guarantee is the CallTool gate below; ListTools just hides what the
|
|
637
|
+
// current context can't use (no tools/list_changed dependency).
|
|
638
|
+
const enabled = loadProjectConfig()?.capabilities?.phaseScopedTools === true;
|
|
639
|
+
if (!enabled) return { tools: TOOLS };
|
|
640
|
+
const profile = _getSessionProfile();
|
|
641
|
+
if (profile === 'orchestrator') return { tools: TOOLS };
|
|
642
|
+
const phase = resolveBoundPhase();
|
|
643
|
+
// target match is unknowable at list time → list a tool if its profile base
|
|
644
|
+
// (ignoring the feature-scoped re-permit) would ever permit it in this phase.
|
|
645
|
+
const tools = TOOLS.filter((t) =>
|
|
646
|
+
isToolAllowed({ tool: t.name, profile, phase, targetMatchesBoundFeature: true }).allowed);
|
|
647
|
+
return { tools };
|
|
648
|
+
});
|
|
633
649
|
|
|
634
650
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
635
651
|
const { name, arguments: args = {} } = request.params;
|
|
636
652
|
|
|
637
653
|
const WORKSPACE_EXEMPT = new Set(['set_workspace', 'get_workspace']);
|
|
638
654
|
|
|
655
|
+
// COMP-MCP-ENFORCE-1: phase-scoped tool gate (hard guarantee). No-op when the
|
|
656
|
+
// capability is off, on a valid override token, or when context is unresolved.
|
|
657
|
+
try {
|
|
658
|
+
assertToolPhaseAllowed(name, args);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
if (err && err.code === 'PHASE_TOOL_DENIED') {
|
|
661
|
+
return {
|
|
662
|
+
content: [{ type: 'text', text: `Error [PHASE_TOOL_DENIED]: ${err.message}` }],
|
|
663
|
+
isError: true,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
throw err;
|
|
667
|
+
}
|
|
668
|
+
|
|
639
669
|
try {
|
|
640
670
|
if (!WORKSPACE_EXEMPT.has(name)) {
|
|
641
671
|
const ws = resolveWorkspace({ getBinding: _getBinding });
|
package/server/design-routes.js
CHANGED
|
@@ -15,6 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { parseDecisionBlocks } from '../src/components/vision/designSessionState.js';
|
|
17
17
|
import { StratumMcpClient } from '../lib/stratum-mcp-client.js';
|
|
18
|
+
import { KNOWN_VERSIONS } from '../lib/build-stream-schema.js';
|
|
18
19
|
|
|
19
20
|
// Lazy singleton — design conversations share one stratum-mcp connection
|
|
20
21
|
// across the server process lifetime. Concurrent runs are correlation-id scoped.
|
|
@@ -144,7 +145,9 @@ ${formattedMessages}`;
|
|
|
144
145
|
const subStepId = '_agent_run';
|
|
145
146
|
|
|
146
147
|
const unsub = stratum.onEvent(correlationId, subStepId, (env) => {
|
|
147
|
-
|
|
148
|
+
// Accept all KNOWN_VERSIONS (producer emits 0.2.6); pinning '0.2.5' dropped
|
|
149
|
+
// the current producer's events. Client already validated before dispatch.
|
|
150
|
+
if (!env || !KNOWN_VERSIONS.has(env.schema_version)) return;
|
|
148
151
|
const m = env.metadata ?? {};
|
|
149
152
|
switch (env.kind) {
|
|
150
153
|
case 'agent_relay':
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// server/mcp-tool-policy.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-MCP-ENFORCE-1 — pure, declarative profile×phase policy for the compose
|
|
4
|
+
// MCP tool gate. No I/O: callers supply the resolved {profile, phase} and whether
|
|
5
|
+
// the tool's target matches the bound feature. See docs/features/COMP-MCP-ENFORCE-1/.
|
|
6
|
+
|
|
7
|
+
/** Agent-template name (server/agent-templates.js) → MCP profile. */
|
|
8
|
+
export const TEMPLATE_PROFILE_MAP = {
|
|
9
|
+
'read-only-reviewer': 'reviewer',
|
|
10
|
+
'read-only-researcher': 'reviewer',
|
|
11
|
+
'security-auditor': 'reviewer',
|
|
12
|
+
'implementer': 'implementer',
|
|
13
|
+
'orchestrator': 'orchestrator',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tools that are NEVER gated — setup/query prerequisites. Denying these would
|
|
18
|
+
* strand a restricted session (e.g. workspace selection precedes feature binding).
|
|
19
|
+
* All read/`get_*`/`validate_*` tools are also implicitly safe but are listed in
|
|
20
|
+
* the reviewer allowlist; this set is the cross-profile exemption.
|
|
21
|
+
*/
|
|
22
|
+
export const SETUP_TOOLS = new Set([
|
|
23
|
+
'set_workspace', 'get_workspace', 'bind_session', 'get_current_session',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Management/approval/completion tools an implementer context must not wield. */
|
|
27
|
+
const IMPLEMENTER_DENY = [
|
|
28
|
+
'approve_gate', 'complete_feature', 'kill_feature',
|
|
29
|
+
'set_feature_status', 'add_roadmap_entry', 'record_completion', 'propose_followup',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Reviewer (read-only) may call only these. */
|
|
33
|
+
const REVIEWER_ALLOW = [
|
|
34
|
+
'get_vision_items', 'get_item_detail', 'get_phase_summary', 'get_blocked_items',
|
|
35
|
+
'get_current_session', 'get_feature_lifecycle', 'get_feature_artifacts', 'get_feature_links',
|
|
36
|
+
'get_pending_gates', 'get_changelog_entries', 'get_journal_entries', 'get_completions',
|
|
37
|
+
'validate_feature', 'validate_project', 'roadmap_diff', 'assess_feature_artifacts',
|
|
38
|
+
'set_workspace', 'get_workspace', 'bind_session',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const PROFILE_POLICY = {
|
|
42
|
+
orchestrator: { mode: 'unrestricted' },
|
|
43
|
+
implementer: { mode: 'deny', tools: new Set(IMPLEMENTER_DENY) },
|
|
44
|
+
reviewer: { mode: 'allowlist', tools: new Set(REVIEWER_ALLOW) },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** phase → management tools re-permitted for DENY-mode profiles in that phase. */
|
|
48
|
+
export const PHASE_REFINEMENT = {
|
|
49
|
+
ship: new Set(['complete_feature', 'record_completion']),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Strictness ordering — a bind hint may only NARROW (raise strictness), never widen.
|
|
53
|
+
const RANK = { orchestrator: 0, implementer: 1, reviewer: 2 };
|
|
54
|
+
function _norm(p) {
|
|
55
|
+
return (p && Object.prototype.hasOwnProperty.call(RANK, p)) ? p : 'orchestrator';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the effective profile. `envProfile` (spawn-injected COMPOSE_SESSION_PROFILE)
|
|
60
|
+
* is the trusted floor; `bindHint` (bind_session arg) may only narrow. Unknown
|
|
61
|
+
* strings normalize to orchestrator (fail-open).
|
|
62
|
+
*/
|
|
63
|
+
export function resolveProfile(envProfile, bindHint) {
|
|
64
|
+
const env = _norm(envProfile);
|
|
65
|
+
const hint = _norm(bindHint);
|
|
66
|
+
const rank = Math.max(RANK[env], RANK[hint]);
|
|
67
|
+
return Object.keys(RANK).find((k) => RANK[k] === rank);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the value to inject as COMPOSE_SESSION_PROFILE when spawning a
|
|
72
|
+
* subagent. Accepts an explicit MCP `profile` or an agent-template `template`
|
|
73
|
+
* (mapped via TEMPLATE_PROFILE_MAP). Returns the restrictive profile string to
|
|
74
|
+
* inject, or null when the result is unrestricted/unknown (inject nothing →
|
|
75
|
+
* the subagent runs as orchestrator, preserving current behavior).
|
|
76
|
+
*/
|
|
77
|
+
export function resolveSpawnProfile({ profile, template } = {}) {
|
|
78
|
+
let p = null;
|
|
79
|
+
if (profile && Object.prototype.hasOwnProperty.call(RANK, profile)) p = profile;
|
|
80
|
+
else if (template && TEMPLATE_PROFILE_MAP[template]) p = TEMPLATE_PROFILE_MAP[template];
|
|
81
|
+
return (p === 'implementer' || p === 'reviewer') ? p : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decide whether a tool may be called in a context.
|
|
86
|
+
* @param {{tool:string, profile:string, phase?:string, targetMatchesBoundFeature?:boolean}} ctx
|
|
87
|
+
* @returns {{allowed:boolean, reason:string}}
|
|
88
|
+
*/
|
|
89
|
+
export function isToolAllowed({ tool, profile, phase, targetMatchesBoundFeature = false }) {
|
|
90
|
+
const prof = _norm(profile);
|
|
91
|
+
const policy = PROFILE_POLICY[prof];
|
|
92
|
+
|
|
93
|
+
if (policy.mode === 'unrestricted') return { allowed: true, reason: 'unrestricted profile' };
|
|
94
|
+
if (SETUP_TOOLS.has(tool)) return { allowed: true, reason: 'setup/query tool (never gated)' };
|
|
95
|
+
|
|
96
|
+
if (policy.mode === 'allowlist') {
|
|
97
|
+
// Phase NEVER widens an allowlist (reviewer stays read-only in every phase).
|
|
98
|
+
return policy.tools.has(tool)
|
|
99
|
+
? { allowed: true, reason: `allowlisted for ${prof}` }
|
|
100
|
+
: { allowed: false, reason: `${tool} not in ${prof} allowlist` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// deny-mode (implementer)
|
|
104
|
+
if (!policy.tools.has(tool)) return { allowed: true, reason: `not denied for ${prof}` };
|
|
105
|
+
const refinement = phase ? PHASE_REFINEMENT[phase] : null;
|
|
106
|
+
if (refinement && refinement.has(tool)) {
|
|
107
|
+
return targetMatchesBoundFeature
|
|
108
|
+
? { allowed: true, reason: `phase '${phase}' re-permits ${tool} on the bound feature` }
|
|
109
|
+
: { allowed: false, reason: `phase '${phase}' re-permits ${tool} only for the bound feature (target differs)` };
|
|
110
|
+
}
|
|
111
|
+
return { allowed: false, reason: `${tool} denied for ${prof} in phase '${phase ?? 'unknown'}'` };
|
|
112
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ai as o,aj as n}from"./App-j8fWZcGr.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-kD07Sqp5.js";import{_ as i}from"./App-j8fWZcGr.js";import"./chunk-FMBD7UC4-Jti_und8.js";import"./chunk-ND2GUHAM-Ipx3noKz.js";import"./chunk-55IACEB6-Bnais1SK.js";import"./chunk-2J33WTMH-CrazA7xu.js";import"./mobile-CG5tLa2S.js";import"./index-uHKnp74B.js";import"./graph-CJVNlri5.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-kD07Sqp5.js";import{_ as i}from"./App-j8fWZcGr.js";import"./chunk-FMBD7UC4-Jti_und8.js";import"./chunk-ND2GUHAM-Ipx3noKz.js";import"./chunk-55IACEB6-Bnais1SK.js";import"./chunk-2J33WTMH-CrazA7xu.js";import"./mobile-CG5tLa2S.js";import"./index-uHKnp74B.js";import"./graph-CJVNlri5.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|