@akira-tl/forgerelay 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/activity/audit-store.js +11 -0
- package/dist/activity/lifecycle.js +111 -0
- package/dist/lsp/test-support/server-fixture.js +6 -1
- package/dist/server.js +214 -124
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.1] - 2026-08-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Routed ForgeRelay's top-level work operations through one persistent Activity lifecycle: read, write, edit, rename, delete, capability, Bash, and Codex-compatible execution/patch operations now record durable started/succeeded/failed/blocked/returned facts without relying on UI inference. Bash/exec process-control follow-ups remain part of the existing semantic operation instead of creating duplicate top-level Activities.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Activity auditing now treats a shell process as `returned` only after its `processId` can actually be delivered to the Host; Host cancellation during post-tool delivery protection records a failed Activity and discards the undelivered process instead of leaving a false returned history entry.
|
|
16
|
+
|
|
7
17
|
## [0.5.0] - 2026-08-14
|
|
8
18
|
|
|
9
19
|
### Added
|
|
@@ -66,6 +66,11 @@ export class ActivityAuditStore {
|
|
|
66
66
|
result = event.result;
|
|
67
67
|
error = undefined;
|
|
68
68
|
break;
|
|
69
|
+
case "returned":
|
|
70
|
+
state = "returned";
|
|
71
|
+
result = event.result;
|
|
72
|
+
error = undefined;
|
|
73
|
+
break;
|
|
69
74
|
case "failed":
|
|
70
75
|
state = "failed";
|
|
71
76
|
result = event.result;
|
|
@@ -177,6 +182,12 @@ function rowToEvent(row) {
|
|
|
177
182
|
type: "succeeded",
|
|
178
183
|
result: parseJson(row.result_json),
|
|
179
184
|
};
|
|
185
|
+
case "returned":
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
type: "returned",
|
|
189
|
+
result: parseJson(row.result_json),
|
|
190
|
+
};
|
|
180
191
|
case "failed":
|
|
181
192
|
if (!row.error)
|
|
182
193
|
throw new Error(`Activity audit failed event ${row.id} is missing an error.`);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { HookExecutionError } from "../hooks.js";
|
|
3
|
+
export class ActivityLifecycle {
|
|
4
|
+
auditStore;
|
|
5
|
+
activityId;
|
|
6
|
+
turnId;
|
|
7
|
+
constructor(auditStore, options = {}) {
|
|
8
|
+
this.auditStore = auditStore;
|
|
9
|
+
this.activityId = options.activityId ?? newActivityId;
|
|
10
|
+
this.turnId = options.turnId ?? newTurnId;
|
|
11
|
+
}
|
|
12
|
+
async run(options) {
|
|
13
|
+
const activityId = options.activityId ?? this.activityId();
|
|
14
|
+
const turnId = options.turnId ?? this.turnId();
|
|
15
|
+
const request = normalizeAuditValue(options.request);
|
|
16
|
+
this.auditStore.append({
|
|
17
|
+
type: "started",
|
|
18
|
+
activityId,
|
|
19
|
+
turnId,
|
|
20
|
+
...(options.conversationScopeId ? { conversationScopeId: options.conversationScopeId } : {}),
|
|
21
|
+
tool: options.tool,
|
|
22
|
+
workspace: options.workspace,
|
|
23
|
+
...(request !== undefined ? { request } : {}),
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const result = await options.operation();
|
|
27
|
+
const normalizedResult = normalizeAuditValue(result);
|
|
28
|
+
const outcome = options.outcome?.(result) ?? { type: "succeeded" };
|
|
29
|
+
switch (outcome.type) {
|
|
30
|
+
case "succeeded":
|
|
31
|
+
case "returned":
|
|
32
|
+
this.auditStore.append({
|
|
33
|
+
type: outcome.type,
|
|
34
|
+
activityId,
|
|
35
|
+
...(normalizedResult !== undefined ? { result: normalizedResult } : {}),
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
case "failed":
|
|
39
|
+
this.auditStore.append({
|
|
40
|
+
type: "failed",
|
|
41
|
+
activityId,
|
|
42
|
+
...(normalizedResult !== undefined ? { result: normalizedResult } : {}),
|
|
43
|
+
error: outcome.error,
|
|
44
|
+
});
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
this.auditStore.append(error instanceof HookExecutionError && error.event === "BeforeTool"
|
|
52
|
+
? { type: "blocked", activityId, error: message }
|
|
53
|
+
: { type: "failed", activityId, error: message });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function newActivityId() {
|
|
59
|
+
return `act_${randomUUID().replaceAll("-", "")}`;
|
|
60
|
+
}
|
|
61
|
+
function newTurnId() {
|
|
62
|
+
return `turn_${randomUUID().replaceAll("-", "")}`;
|
|
63
|
+
}
|
|
64
|
+
export function normalizeAuditValue(value) {
|
|
65
|
+
return normalizeAuditValueInternal(value, new WeakSet());
|
|
66
|
+
}
|
|
67
|
+
function normalizeAuditValueInternal(value, seen) {
|
|
68
|
+
if (value === undefined || typeof value === "function" || typeof value === "symbol")
|
|
69
|
+
return undefined;
|
|
70
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
71
|
+
return value;
|
|
72
|
+
if (typeof value === "number")
|
|
73
|
+
return Number.isFinite(value) ? value : String(value);
|
|
74
|
+
if (typeof value === "bigint")
|
|
75
|
+
return value.toString();
|
|
76
|
+
if (value instanceof Date)
|
|
77
|
+
return value.toISOString();
|
|
78
|
+
if (value instanceof Error) {
|
|
79
|
+
return {
|
|
80
|
+
name: value.name,
|
|
81
|
+
message: value.message,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (value instanceof Uint8Array) {
|
|
85
|
+
return {
|
|
86
|
+
type: "bytes",
|
|
87
|
+
encoding: "base64",
|
|
88
|
+
data: Buffer.from(value).toString("base64"),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
return value.map((entry) => normalizeAuditValueInternal(entry, seen) ?? null);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value !== "object")
|
|
95
|
+
return String(value);
|
|
96
|
+
if (seen.has(value))
|
|
97
|
+
return "[Circular]";
|
|
98
|
+
seen.add(value);
|
|
99
|
+
try {
|
|
100
|
+
const normalized = {};
|
|
101
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
102
|
+
const next = normalizeAuditValueInternal(entry, seen);
|
|
103
|
+
if (next !== undefined)
|
|
104
|
+
normalized[key] = next;
|
|
105
|
+
}
|
|
106
|
+
return normalized;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
seen.delete(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -4,6 +4,8 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
6
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
+
import { ActivityAuditStore } from "../../activity/audit-store.js";
|
|
8
|
+
import { ActivityLifecycle } from "../../activity/lifecycle.js";
|
|
7
9
|
import { loadConfig } from "../../config.js";
|
|
8
10
|
import { createReviewCheckpointManager } from "../../review-checkpoints.js";
|
|
9
11
|
import { ProcessManager } from "../../process-sessions.js";
|
|
@@ -33,8 +35,10 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
33
35
|
const store = new SqliteWorkspaceStore(stateDir);
|
|
34
36
|
const workspaces = new WorkspaceRegistry(config, store);
|
|
35
37
|
const processSessions = new ProcessManager();
|
|
38
|
+
const auditStore = new ActivityAuditStore(stateDir);
|
|
39
|
+
const activityLifecycle = new ActivityLifecycle(auditStore);
|
|
36
40
|
const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
|
|
37
|
-
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence);
|
|
41
|
+
const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle);
|
|
38
42
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
39
43
|
const client = new Client({ name: "forgerelay-code-intelligence-test-client", version: "1.0.0" });
|
|
40
44
|
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
|
|
@@ -47,6 +51,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
47
51
|
await server.close();
|
|
48
52
|
await codeIntelligence.shutdown();
|
|
49
53
|
processSessions.shutdown();
|
|
54
|
+
auditStore.close();
|
|
50
55
|
store.close();
|
|
51
56
|
};
|
|
52
57
|
t.after(async () => {
|
package/dist/server.js
CHANGED
|
@@ -15,6 +15,8 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@mode
|
|
|
15
15
|
import express from "express";
|
|
16
16
|
import * as z from "zod/v4";
|
|
17
17
|
import { applyPatch } from "./apply-patch.js";
|
|
18
|
+
import { ActivityAuditStore } from "./activity/audit-store.js";
|
|
19
|
+
import { ActivityLifecycle } from "./activity/lifecycle.js";
|
|
18
20
|
import { buildCapabilityFingerprint } from "./capabilities.js";
|
|
19
21
|
import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
|
|
20
22
|
import { deletePath, renamePath } from "./file-mutations.js";
|
|
@@ -597,7 +599,77 @@ async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
|
|
|
597
599
|
function toolResultIsError(result) {
|
|
598
600
|
return typeof result === "object" && result !== null && result.isError === true;
|
|
599
601
|
}
|
|
600
|
-
function
|
|
602
|
+
function workspaceActivitySnapshot(workspace) {
|
|
603
|
+
return {
|
|
604
|
+
id: workspace.id,
|
|
605
|
+
root: workspace.root,
|
|
606
|
+
mode: workspace.mode,
|
|
607
|
+
...(workspace.sourceRoot ? { sourceRoot: workspace.sourceRoot } : {}),
|
|
608
|
+
...(workspace.worktree?.branch ? { branch: workspace.worktree.branch } : {}),
|
|
609
|
+
...(workspace.worktree?.targetBranch ? { targetBranch: workspace.worktree.targetBranch } : {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function activityFailureMessage(result) {
|
|
613
|
+
if (typeof result !== "object" || result === null)
|
|
614
|
+
return "Tool returned a failed result.";
|
|
615
|
+
const record = result;
|
|
616
|
+
if (Array.isArray(record.content)) {
|
|
617
|
+
const text = record.content
|
|
618
|
+
.map((entry) => {
|
|
619
|
+
if (typeof entry !== "object" || entry === null)
|
|
620
|
+
return "";
|
|
621
|
+
const value = entry.text;
|
|
622
|
+
return typeof value === "string" ? value : "";
|
|
623
|
+
})
|
|
624
|
+
.filter(Boolean)
|
|
625
|
+
.join("\n");
|
|
626
|
+
if (text)
|
|
627
|
+
return text;
|
|
628
|
+
}
|
|
629
|
+
if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
|
|
630
|
+
const value = record.structuredContent.result;
|
|
631
|
+
if (typeof value === "string" && value)
|
|
632
|
+
return value;
|
|
633
|
+
}
|
|
634
|
+
return "Tool returned a failed result.";
|
|
635
|
+
}
|
|
636
|
+
function standardActivityOutcome(result) {
|
|
637
|
+
return toolResultIsError(result)
|
|
638
|
+
? { type: "failed", error: activityFailureMessage(result) }
|
|
639
|
+
: { type: "succeeded" };
|
|
640
|
+
}
|
|
641
|
+
function processActivityOutcome(result) {
|
|
642
|
+
if (toolResultIsError(result))
|
|
643
|
+
return { type: "failed", error: activityFailureMessage(result) };
|
|
644
|
+
if (typeof result !== "object" || result === null)
|
|
645
|
+
return { type: "succeeded" };
|
|
646
|
+
const structured = result.structuredContent;
|
|
647
|
+
if (typeof structured !== "object" || structured === null)
|
|
648
|
+
return { type: "succeeded" };
|
|
649
|
+
const process = structured;
|
|
650
|
+
if (process.running === true)
|
|
651
|
+
return { type: "returned" };
|
|
652
|
+
if (process.timedOut === true ||
|
|
653
|
+
typeof process.signal === "string" ||
|
|
654
|
+
(typeof process.exitCode === "number" && process.exitCode !== 0)) {
|
|
655
|
+
return { type: "failed", error: activityFailureMessage(result) };
|
|
656
|
+
}
|
|
657
|
+
return { type: "succeeded" };
|
|
658
|
+
}
|
|
659
|
+
function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome) {
|
|
660
|
+
return lifecycle.run({
|
|
661
|
+
tool,
|
|
662
|
+
workspace: workspaceActivitySnapshot(workspace),
|
|
663
|
+
conversationScopeId: openAiConversationScopeId(requestMeta),
|
|
664
|
+
request,
|
|
665
|
+
operation,
|
|
666
|
+
outcome,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions) {
|
|
670
|
+
return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions));
|
|
671
|
+
}
|
|
672
|
+
function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle) {
|
|
601
673
|
if (config.toolMode === "codex") {
|
|
602
674
|
registerAppTool(server, "exec_command", {
|
|
603
675
|
title: "Execute command",
|
|
@@ -643,61 +715,63 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
643
715
|
}, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
|
|
644
716
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
645
717
|
let undeliveredProcessId;
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
catch (error) {
|
|
696
|
-
if (undeliveredProcessId !== undefined) {
|
|
697
|
-
processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
|
|
718
|
+
return runActivityTool(activityLifecycle, workspace, extra._meta, "exec_command", { workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, async () => {
|
|
719
|
+
try {
|
|
720
|
+
const result = await runToolWithHooks(hooks, {
|
|
721
|
+
signal: extra.signal,
|
|
722
|
+
tool: "exec_command",
|
|
723
|
+
invocation: workspaceHookInvocation(workspace),
|
|
724
|
+
payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
|
|
725
|
+
operation: async () => {
|
|
726
|
+
const startedAt = performance.now();
|
|
727
|
+
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
728
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
|
|
729
|
+
const snapshot = await processSessions.start({
|
|
730
|
+
workspaceId,
|
|
731
|
+
command: cmd,
|
|
732
|
+
cwd,
|
|
733
|
+
workspaceRoot: workspace.root,
|
|
734
|
+
tty,
|
|
735
|
+
columns,
|
|
736
|
+
rows,
|
|
737
|
+
yieldTimeMs,
|
|
738
|
+
timeoutMs,
|
|
739
|
+
maxOutputTokens,
|
|
740
|
+
codexCi: true,
|
|
741
|
+
signal: extra.signal,
|
|
742
|
+
});
|
|
743
|
+
undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
|
|
744
|
+
logToolCall(config, {
|
|
745
|
+
tool: "exec_command",
|
|
746
|
+
...workspaceLogContext(workspace, extra.sessionId),
|
|
747
|
+
workingDirectory: workingDirectory ?? ".",
|
|
748
|
+
command: cmd,
|
|
749
|
+
commandLength: cmd.length,
|
|
750
|
+
exitCode: snapshot.exitCode,
|
|
751
|
+
running: snapshot.running,
|
|
752
|
+
processId: snapshot.processId,
|
|
753
|
+
success: snapshot.running || snapshot.exitCode === 0,
|
|
754
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
755
|
+
});
|
|
756
|
+
return processToolResponse("exec_command", workspaceId, snapshot, {
|
|
757
|
+
command: cmd,
|
|
758
|
+
workingDirectory: workingDirectory ?? ".",
|
|
759
|
+
running: snapshot.running,
|
|
760
|
+
exitCode: snapshot.exitCode,
|
|
761
|
+
wallTimeMs: snapshot.wallTimeMs,
|
|
762
|
+
});
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
extra.signal.throwIfAborted();
|
|
766
|
+
return result;
|
|
698
767
|
}
|
|
699
|
-
|
|
700
|
-
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (undeliveredProcessId !== undefined) {
|
|
770
|
+
processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
}, processActivityOutcome);
|
|
701
775
|
});
|
|
702
776
|
}
|
|
703
777
|
if (config.toolMode !== "codex")
|
|
@@ -775,7 +849,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
775
849
|
});
|
|
776
850
|
});
|
|
777
851
|
}
|
|
778
|
-
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence) {
|
|
852
|
+
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle) {
|
|
779
853
|
const toolDescriptions = buildToolDescriptions(config);
|
|
780
854
|
const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result));
|
|
781
855
|
const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
|
|
@@ -1287,7 +1361,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1287
1361
|
}, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
|
|
1288
1362
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1289
1363
|
let changedPaths = [];
|
|
1290
|
-
return
|
|
1364
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, name, action, arguments: capabilityArguments, file }, {
|
|
1291
1365
|
signal: extra.signal,
|
|
1292
1366
|
tool: toolNames.capability,
|
|
1293
1367
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1510,7 +1584,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1510
1584
|
annotations: { readOnlyHint: true },
|
|
1511
1585
|
}, async ({ workspaceId, ...input }, extra) => {
|
|
1512
1586
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1513
|
-
return
|
|
1587
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
|
|
1514
1588
|
signal: extra.signal,
|
|
1515
1589
|
tool: toolNames.read,
|
|
1516
1590
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1597,7 +1671,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1597
1671
|
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
1598
1672
|
}, async ({ workspaceId, ...input }, extra) => {
|
|
1599
1673
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1600
|
-
return
|
|
1674
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
|
|
1601
1675
|
signal: extra.signal,
|
|
1602
1676
|
tool: toolNames.write,
|
|
1603
1677
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1681,7 +1755,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1681
1755
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
1682
1756
|
}, async ({ workspaceId, ...input }, extra) => {
|
|
1683
1757
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1684
|
-
return
|
|
1758
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
|
|
1685
1759
|
signal: extra.signal,
|
|
1686
1760
|
tool: toolNames.edit,
|
|
1687
1761
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1758,7 +1832,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1758
1832
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
1759
1833
|
}, async ({ workspaceId, path, newPath }, extra) => {
|
|
1760
1834
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1761
|
-
return
|
|
1835
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, newPath }, {
|
|
1762
1836
|
signal: extra.signal,
|
|
1763
1837
|
tool: toolNames.rename,
|
|
1764
1838
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1831,7 +1905,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1831
1905
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
1832
1906
|
}, async ({ workspaceId, path, recursive }, extra) => {
|
|
1833
1907
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1834
|
-
return
|
|
1908
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, recursive }, {
|
|
1835
1909
|
signal: extra.signal,
|
|
1836
1910
|
tool: toolNames.delete,
|
|
1837
1911
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -1912,7 +1986,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1912
1986
|
annotations: EDIT_TOOL_ANNOTATIONS,
|
|
1913
1987
|
}, async ({ workspaceId, patch }, extra) => {
|
|
1914
1988
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1915
|
-
return
|
|
1989
|
+
return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, patch }, {
|
|
1916
1990
|
signal: extra.signal,
|
|
1917
1991
|
tool: "apply_patch",
|
|
1918
1992
|
invocation: workspaceHookInvocation(workspace),
|
|
@@ -2048,69 +2122,82 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2048
2122
|
throw new Error("bash action=run does not accept processId, input, or interrupt.");
|
|
2049
2123
|
}
|
|
2050
2124
|
let undeliveredProcessId;
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
cwd,
|
|
2070
|
-
workspaceRoot: workspace.root,
|
|
2071
|
-
tty,
|
|
2072
|
-
columns,
|
|
2073
|
-
rows,
|
|
2074
|
-
yieldTimeMs,
|
|
2075
|
-
timeoutMs,
|
|
2076
|
-
maxOutputTokens,
|
|
2077
|
-
signal: extra.signal,
|
|
2078
|
-
});
|
|
2079
|
-
undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
|
|
2080
|
-
logToolCall(config, {
|
|
2081
|
-
tool: toolNames.shell,
|
|
2082
|
-
...workspaceLogContext(workspace, extra.sessionId),
|
|
2083
|
-
workingDirectory: workingDirectory ?? ".",
|
|
2084
|
-
command,
|
|
2085
|
-
commandLength: command.length,
|
|
2086
|
-
exitCode: snapshot.exitCode,
|
|
2087
|
-
running: snapshot.running,
|
|
2088
|
-
processId: snapshot.processId,
|
|
2089
|
-
success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
|
|
2090
|
-
durationMs: Math.round(performance.now() - startedAt),
|
|
2091
|
-
});
|
|
2092
|
-
const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
|
|
2125
|
+
return runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.shell, {
|
|
2126
|
+
workspaceId,
|
|
2127
|
+
action,
|
|
2128
|
+
command,
|
|
2129
|
+
tty,
|
|
2130
|
+
columns,
|
|
2131
|
+
rows,
|
|
2132
|
+
workingDirectory,
|
|
2133
|
+
yieldTimeMs,
|
|
2134
|
+
timeoutMs,
|
|
2135
|
+
maxOutputTokens,
|
|
2136
|
+
}, async () => {
|
|
2137
|
+
try {
|
|
2138
|
+
const result = await runToolWithHooks(hooks, {
|
|
2139
|
+
signal: extra.signal,
|
|
2140
|
+
tool: toolNames.shell,
|
|
2141
|
+
invocation: workspaceHookInvocation(workspace),
|
|
2142
|
+
payload: {
|
|
2093
2143
|
action,
|
|
2094
2144
|
command,
|
|
2095
2145
|
workingDirectory: workingDirectory ?? ".",
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2146
|
+
},
|
|
2147
|
+
isFailure: toolResultIsError,
|
|
2148
|
+
operation: async () => {
|
|
2149
|
+
const startedAt = performance.now();
|
|
2150
|
+
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
2151
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
|
|
2152
|
+
const snapshot = await processSessions.start({
|
|
2153
|
+
workspaceId,
|
|
2154
|
+
command,
|
|
2155
|
+
cwd,
|
|
2156
|
+
workspaceRoot: workspace.root,
|
|
2157
|
+
tty,
|
|
2158
|
+
columns,
|
|
2159
|
+
rows,
|
|
2160
|
+
yieldTimeMs,
|
|
2161
|
+
timeoutMs,
|
|
2162
|
+
maxOutputTokens,
|
|
2163
|
+
signal: extra.signal,
|
|
2164
|
+
});
|
|
2165
|
+
undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
|
|
2166
|
+
logToolCall(config, {
|
|
2167
|
+
tool: toolNames.shell,
|
|
2168
|
+
...workspaceLogContext(workspace, extra.sessionId),
|
|
2169
|
+
workingDirectory: workingDirectory ?? ".",
|
|
2170
|
+
command,
|
|
2171
|
+
commandLength: command.length,
|
|
2172
|
+
exitCode: snapshot.exitCode,
|
|
2173
|
+
running: snapshot.running,
|
|
2174
|
+
processId: snapshot.processId,
|
|
2175
|
+
success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
|
|
2176
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2177
|
+
});
|
|
2178
|
+
const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
|
|
2179
|
+
action,
|
|
2180
|
+
command,
|
|
2181
|
+
workingDirectory: workingDirectory ?? ".",
|
|
2182
|
+
running: snapshot.running,
|
|
2183
|
+
exitCode: snapshot.exitCode,
|
|
2184
|
+
wallTimeMs: snapshot.wallTimeMs,
|
|
2185
|
+
});
|
|
2186
|
+
return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
|
|
2187
|
+
? { ...response, isError: true }
|
|
2188
|
+
: response;
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
extra.signal.throwIfAborted();
|
|
2192
|
+
return result;
|
|
2111
2193
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2194
|
+
catch (error) {
|
|
2195
|
+
if (undeliveredProcessId !== undefined) {
|
|
2196
|
+
processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
|
|
2197
|
+
}
|
|
2198
|
+
throw error;
|
|
2199
|
+
}
|
|
2200
|
+
}, processActivityOutcome);
|
|
2114
2201
|
}
|
|
2115
2202
|
if (command !== undefined || workingDirectory !== undefined || tty !== undefined || timeoutMs !== undefined) {
|
|
2116
2203
|
throw new Error("bash action=process does not accept command, workingDirectory, tty, or timeoutMs.");
|
|
@@ -2167,7 +2254,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2167
2254
|
});
|
|
2168
2255
|
});
|
|
2169
2256
|
}
|
|
2170
|
-
registerProcessTools(server, config, workspaces, processSessions, hooks);
|
|
2257
|
+
registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle);
|
|
2171
2258
|
return server;
|
|
2172
2259
|
}
|
|
2173
2260
|
export function createServer(config = loadConfig(), options = {}) {
|
|
@@ -2193,6 +2280,8 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
2193
2280
|
});
|
|
2194
2281
|
const workspaceStore = createWorkspaceStore(config.stateDir);
|
|
2195
2282
|
const workspaces = new WorkspaceRegistry(config, workspaceStore);
|
|
2283
|
+
const activityAuditStore = new ActivityAuditStore(config.stateDir);
|
|
2284
|
+
const activityLifecycle = new ActivityLifecycle(activityAuditStore);
|
|
2196
2285
|
const reviewCheckpoints = createReviewCheckpointManager();
|
|
2197
2286
|
const processSessions = new ProcessManager();
|
|
2198
2287
|
const codeIntelligence = new CodeIntelligenceManager(config);
|
|
@@ -2379,7 +2468,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
2379
2468
|
});
|
|
2380
2469
|
}
|
|
2381
2470
|
};
|
|
2382
|
-
const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence);
|
|
2471
|
+
const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle);
|
|
2383
2472
|
await server.connect(transport);
|
|
2384
2473
|
}
|
|
2385
2474
|
else {
|
|
@@ -2411,6 +2500,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
2411
2500
|
processSessions.shutdown();
|
|
2412
2501
|
await codeIntelligence.shutdown();
|
|
2413
2502
|
oauthProvider.close();
|
|
2503
|
+
activityAuditStore.close();
|
|
2414
2504
|
workspaceStore.close?.();
|
|
2415
2505
|
})();
|
|
2416
2506
|
return closePromise;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"release:parity": "node scripts/release-parity.mjs",
|
|
45
45
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
46
46
|
"start": "node dist/cli.js serve",
|
|
47
|
-
"test": "node --test scripts/release-proof.test.mjs && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/activity/audit-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
47
|
+
"test": "node --test scripts/release-proof.test.mjs && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/activity/audit-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
48
48
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
49
|
"release:check": "node scripts/release-version.mjs check",
|
|
50
50
|
"release:tag-check": "node scripts/release-version.mjs tag",
|