@tt-a1i/hive 1.7.0 → 2.0.2
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 +60 -0
- package/README.en.md +73 -11
- package/README.md +41 -8
- package/dist/src/cli/hive-remote.d.ts +46 -0
- package/dist/src/cli/hive-remote.js +257 -0
- package/dist/src/cli/hive-update.js +7 -2
- package/dist/src/cli/hive.d.ts +6 -0
- package/dist/src/cli/hive.js +64 -0
- package/dist/src/cli/team.d.ts +22 -0
- package/dist/src/cli/team.js +255 -5
- package/dist/src/server/agent-command-resolver.js +10 -3
- package/dist/src/server/agent-exit-classification.d.ts +6 -0
- package/dist/src/server/agent-exit-classification.js +6 -0
- package/dist/src/server/agent-manager-support.d.ts +2 -1
- package/dist/src/server/agent-manager-support.js +59 -15
- package/dist/src/server/agent-manager.d.ts +3 -0
- package/dist/src/server/agent-manager.js +22 -7
- package/dist/src/server/agent-run-bootstrap.d.ts +14 -0
- package/dist/src/server/agent-run-bootstrap.js +11 -4
- package/dist/src/server/agent-run-exit-handler.js +14 -8
- package/dist/src/server/agent-run-starter.d.ts +3 -1
- package/dist/src/server/agent-run-starter.js +22 -5
- package/dist/src/server/agent-run-sync.js +13 -5
- package/dist/src/server/agent-runtime-types.d.ts +1 -0
- package/dist/src/server/agent-runtime.d.ts +2 -1
- package/dist/src/server/agent-runtime.js +9 -2
- package/dist/src/server/agent-startup-instructions.d.ts +2 -1
- package/dist/src/server/agent-startup-instructions.js +8 -4
- package/dist/src/server/agent-stdin-dispatcher.d.ts +4 -2
- package/dist/src/server/agent-stdin-dispatcher.js +35 -3
- package/dist/src/server/command-preset-defaults.d.ts +6 -1
- package/dist/src/server/command-preset-defaults.js +56 -0
- package/dist/src/server/fs-browse.d.ts +2 -0
- package/dist/src/server/fs-browse.js +165 -31
- package/dist/src/server/fs-pick-folder.js +6 -69
- package/dist/src/server/fs-sandbox.d.ts +5 -3
- package/dist/src/server/fs-sandbox.js +5 -3
- package/dist/src/server/hive-team-guidance.js +18 -6
- package/dist/src/server/machine-name.d.ts +2 -0
- package/dist/src/server/machine-name.js +13 -0
- package/dist/src/server/open-target-commands.d.ts +1 -0
- package/dist/src/server/open-target-commands.js +4 -1
- package/dist/src/server/orchestrator-autostart.js +1 -1
- package/dist/src/server/platform-path.d.ts +1 -0
- package/dist/src/server/platform-path.js +14 -1
- package/dist/src/server/post-start-input-writer.js +50 -13
- package/dist/src/server/preset-launch-support.js +1 -0
- package/dist/src/server/recovery-summary.d.ts +2 -1
- package/dist/src/server/recovery-summary.js +2 -1
- package/dist/src/server/remote-audit-store.d.ts +51 -0
- package/dist/src/server/remote-audit-store.js +108 -0
- package/dist/src/server/remote-config-keys.d.ts +17 -0
- package/dist/src/server/remote-config-keys.js +27 -0
- package/dist/src/server/remote-control-constants.d.ts +30 -0
- package/dist/src/server/remote-control-constants.js +29 -0
- package/dist/src/server/remote-device-session.d.ts +40 -0
- package/dist/src/server/remote-device-session.js +22 -0
- package/dist/src/server/remote-device-store.d.ts +36 -0
- package/dist/src/server/remote-device-store.js +67 -0
- package/dist/src/server/remote-frame-bridge.d.ts +102 -0
- package/dist/src/server/remote-frame-bridge.js +791 -0
- package/dist/src/server/remote-gateway-client.d.ts +14 -0
- package/dist/src/server/remote-gateway-client.js +36 -0
- package/dist/src/server/remote-loopback-auth.d.ts +6 -0
- package/dist/src/server/remote-loopback-auth.js +112 -0
- package/dist/src/server/remote-pairing-tunnel.d.ts +59 -0
- package/dist/src/server/remote-pairing-tunnel.js +146 -0
- package/dist/src/server/remote-pairing.d.ts +58 -0
- package/dist/src/server/remote-pairing.js +237 -0
- package/dist/src/server/remote-tunnel.d.ts +113 -0
- package/dist/src/server/remote-tunnel.js +514 -0
- package/dist/src/server/restart-policy-support.d.ts +4 -1
- package/dist/src/server/restart-policy-support.js +3 -1
- package/dist/src/server/restart-policy.d.ts +1 -1
- package/dist/src/server/restart-policy.js +19 -3
- package/dist/src/server/route-types.d.ts +1 -1
- package/dist/src/server/routes-dispatches.js +1 -1
- package/dist/src/server/routes-fs.js +3 -3
- package/dist/src/server/routes-marketplace.js +2 -2
- package/dist/src/server/routes-open-workspace.js +1 -1
- package/dist/src/server/routes-remote.d.ts +2 -0
- package/dist/src/server/routes-remote.js +166 -0
- package/dist/src/server/routes-runtime.js +6 -6
- package/dist/src/server/routes-settings.js +16 -16
- package/dist/src/server/routes-tasks.js +2 -2
- package/dist/src/server/routes-team-memory.d.ts +2 -0
- package/dist/src/server/routes-team-memory.js +154 -0
- package/dist/src/server/routes-team-recall.d.ts +2 -0
- package/dist/src/server/routes-team-recall.js +119 -0
- package/dist/src/server/routes-team.js +31 -9
- package/dist/src/server/routes-ui.js +11 -1
- package/dist/src/server/routes-workflow-schedules.js +3 -3
- package/dist/src/server/routes-workflows.js +5 -5
- package/dist/src/server/routes-workspace-memory-dreams.d.ts +2 -0
- package/dist/src/server/routes-workspace-memory-dreams.js +105 -0
- package/dist/src/server/routes-workspace-memory.d.ts +2 -0
- package/dist/src/server/routes-workspace-memory.js +215 -0
- package/dist/src/server/routes-workspaces.js +9 -9
- package/dist/src/server/routes.js +10 -0
- package/dist/src/server/runtime-database.d.ts +1 -0
- package/dist/src/server/runtime-database.js +27 -2
- package/dist/src/server/runtime-restart-policy.d.ts +3 -1
- package/dist/src/server/runtime-restart-policy.js +2 -1
- package/dist/src/server/runtime-store-contract.d.ts +37 -0
- package/dist/src/server/runtime-store-dream.d.ts +23 -0
- package/dist/src/server/runtime-store-dream.js +16 -0
- package/dist/src/server/runtime-store-helpers.d.ts +20 -0
- package/dist/src/server/runtime-store-helpers.js +81 -7
- package/dist/src/server/runtime-store-memory.d.ts +33 -0
- package/dist/src/server/runtime-store-memory.js +37 -0
- package/dist/src/server/runtime-store-remote.d.ts +5 -0
- package/dist/src/server/runtime-store-remote.js +45 -0
- package/dist/src/server/runtime-store-workflows.js +2 -0
- package/dist/src/server/runtime-store.js +14 -3
- package/dist/src/server/session-capture-claude.d.ts +1 -1
- package/dist/src/server/session-capture-claude.js +7 -4
- package/dist/src/server/session-capture-codex.js +4 -5
- package/dist/src/server/session-capture-gemini.js +4 -5
- package/dist/src/server/session-capture-opencode.d.ts +4 -4
- package/dist/src/server/session-capture-opencode.js +20 -12
- package/dist/src/server/session-capture-qwen.d.ts +5 -0
- package/dist/src/server/session-capture-qwen.js +104 -0
- package/dist/src/server/session-capture.d.ts +17 -0
- package/dist/src/server/session-capture.js +16 -0
- package/dist/src/server/sqlite-schema-v23.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v23.js +43 -0
- package/dist/src/server/sqlite-schema-v24.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v24.js +34 -0
- package/dist/src/server/sqlite-schema-v25.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v25.js +127 -0
- package/dist/src/server/sqlite-schema-v26.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v26.js +56 -0
- package/dist/src/server/sqlite-schema-v27.d.ts +6 -0
- package/dist/src/server/sqlite-schema-v27.js +92 -0
- package/dist/src/server/sqlite-schema-v28.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v28.js +19 -0
- package/dist/src/server/sqlite-schema-v29.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v29.js +27 -0
- package/dist/src/server/sqlite-schema-v30.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v30.js +27 -0
- package/dist/src/server/sqlite-schema-v31.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v31.js +30 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +49 -1
- package/dist/src/server/startup-command-parser.js +5 -1
- package/dist/src/server/tasks-file-watcher.d.ts +2 -0
- package/dist/src/server/tasks-file-watcher.js +15 -6
- package/dist/src/server/tasks-file.js +30 -5
- package/dist/src/server/tasks-websocket-server.js +4 -0
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +13 -1
- package/dist/src/server/team-list-enrichment.js +3 -1
- package/dist/src/server/team-memory-digest.d.ts +52 -0
- package/dist/src/server/team-memory-digest.js +200 -0
- package/dist/src/server/team-memory-dream-applier.d.ts +5 -0
- package/dist/src/server/team-memory-dream-applier.js +234 -0
- package/dist/src/server/team-memory-dream-http-serializers.d.ts +13 -0
- package/dist/src/server/team-memory-dream-http-serializers.js +12 -0
- package/dist/src/server/team-memory-dream-ops.d.ts +40 -0
- package/dist/src/server/team-memory-dream-ops.js +153 -0
- package/dist/src/server/team-memory-dream-reverter.d.ts +22 -0
- package/dist/src/server/team-memory-dream-reverter.js +221 -0
- package/dist/src/server/team-memory-dream-run-store.d.ts +23 -0
- package/dist/src/server/team-memory-dream-run-store.js +211 -0
- package/dist/src/server/team-memory-dream-runner.d.ts +37 -0
- package/dist/src/server/team-memory-dream-runner.js +178 -0
- package/dist/src/server/team-memory-dream-scheduler.d.ts +32 -0
- package/dist/src/server/team-memory-dream-scheduler.js +115 -0
- package/dist/src/server/team-memory-dream-store.d.ts +19 -0
- package/dist/src/server/team-memory-dream-store.js +16 -0
- package/dist/src/server/team-memory-dream-types.d.ts +104 -0
- package/dist/src/server/team-memory-dream-types.js +23 -0
- package/dist/src/server/team-memory-export.d.ts +22 -0
- package/dist/src/server/team-memory-export.js +220 -0
- package/dist/src/server/team-memory-feature.d.ts +12 -0
- package/dist/src/server/team-memory-feature.js +12 -0
- package/dist/src/server/team-memory-http-serializers.d.ts +102 -0
- package/dist/src/server/team-memory-http-serializers.js +46 -0
- package/dist/src/server/team-memory-injection.d.ts +31 -0
- package/dist/src/server/team-memory-injection.js +49 -0
- package/dist/src/server/team-memory-store.d.ts +116 -0
- package/dist/src/server/team-memory-store.js +513 -0
- package/dist/src/server/team-operations.d.ts +5 -1
- package/dist/src/server/team-operations.js +46 -16
- package/dist/src/server/team-recall-store.d.ts +38 -0
- package/dist/src/server/team-recall-store.js +205 -0
- package/dist/src/server/terminal-input-profile.d.ts +1 -1
- package/dist/src/server/terminal-input-profile.js +18 -0
- package/dist/src/server/terminal-ws-server.js +6 -0
- package/dist/src/server/ui-auth-helpers.d.ts +1 -1
- package/dist/src/server/ui-auth-helpers.js +7 -1
- package/dist/src/server/ui-auth.d.ts +3 -0
- package/dist/src/server/ui-auth.js +21 -1
- package/dist/src/server/workflow-cli-policy.d.ts +2 -3
- package/dist/src/server/workflow-cli-policy.js +3 -3
- package/dist/src/server/workflow-runner.d.ts +1 -0
- package/dist/src/server/workflow-runner.js +9 -4
- package/dist/src/server/workspace-path-validation.js +6 -2
- package/dist/src/server/workspace-store.d.ts +1 -1
- package/dist/src/server/workspace-store.js +35 -9
- package/dist/src/shared/fs-browse.d.ts +1 -0
- package/dist/src/shared/fs-browse.js +1 -0
- package/dist/src/shared/path-input.d.ts +12 -0
- package/dist/src/shared/path-input.js +22 -0
- package/dist/src/shared/remote-bridge-routing.d.ts +19 -0
- package/dist/src/shared/remote-bridge-routing.js +141 -0
- package/dist/src/shared/remote-crypto.d.ts +138 -0
- package/dist/src/shared/remote-crypto.js +427 -0
- package/dist/src/shared/remote-pairing-code.d.ts +7 -0
- package/dist/src/shared/remote-pairing-code.js +47 -0
- package/dist/src/shared/remote-protocol.d.ts +160 -0
- package/dist/src/shared/remote-protocol.js +526 -0
- package/dist/src/shared/team-memory.d.ts +11 -0
- package/dist/src/shared/team-memory.js +10 -0
- package/dist/src/shared/team-recall.d.ts +1 -0
- package/dist/src/shared/team-recall.js +1 -0
- package/dist/src/shared/types.d.ts +4 -5
- package/package.json +12 -5
- package/scripts/postinstall-native-artifacts.mjs +113 -0
- package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +2 -0
- package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +1 -0
- package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +1 -0
- package/web/dist/assets/MarketplaceDrawer-Dd8WIA8T.js +67 -0
- package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +1 -0
- package/web/dist/assets/{WhatsNewDialog-CHkZeINH.js → WhatsNewDialog-C2VZaip0.js} +1 -1
- package/web/dist/assets/WorkerModal-DucW-9YT.js +1 -0
- package/web/dist/assets/WorkflowsDrawer-Bjf4olbR.js +1 -0
- package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +1 -0
- package/web/dist/assets/WorkspaceTaskDrawer-BIWwISvA.js +1 -0
- package/web/dist/assets/index-BAiLYajK.css +1 -0
- package/web/dist/assets/index-BV2k9Dts.js +73 -0
- package/web/dist/assets/search-Bk2HQvO7.js +1 -0
- package/web/dist/assets/square-terminal-D93m9hfY.js +1 -0
- package/web/dist/cli-icons/agy.png +0 -0
- package/web/dist/cli-icons/cursor.ico +0 -0
- package/web/dist/cli-icons/grok.ico +0 -0
- package/web/dist/cli-icons/qwen.png +0 -0
- package/web/dist/index.html +8 -3
- package/web/dist/sw.js +1 -1
- package/scripts/fix-runtime-artifacts.mjs +0 -33
- package/web/dist/assets/AddWorkerDialog-BRUxpa3f.js +0 -2
- package/web/dist/assets/AddWorkspaceDialog-D56x5JCb.js +0 -1
- package/web/dist/assets/FirstRunWizard-BFVaMIsE.js +0 -1
- package/web/dist/assets/MarketplaceDrawer-DeEZ35dN.js +0 -76
- package/web/dist/assets/WorkerModal-BBCuMLIa.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CpZHAcj1.js +0 -1
- package/web/dist/assets/WorkspaceTerminalPanels-7If2mDyp.js +0 -1
- package/web/dist/assets/WorkspaceTerminalPanels-DDGTF8rc.css +0 -1
- package/web/dist/assets/index-5zh61jMg.css +0 -1
- package/web/dist/assets/index-CxNL0O-C.js +0 -73
- package/web/dist/assets/path-join-7MR1s7b1.js +0 -1
|
@@ -2,6 +2,7 @@ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
|
|
|
2
2
|
import { buildRecoverySummary } from './recovery-summary.js';
|
|
3
3
|
import { findPreviousRun, writeSystemMessage, } from './restart-policy-support.js';
|
|
4
4
|
import { createSystemRecoverySummaryMessage } from './runtime-message-builders.js';
|
|
5
|
+
import { buildMemoryDigestSafely, logMemoryDigestInjection, rollbackMemoryDigestInjection, } from './team-memory-injection.js';
|
|
5
6
|
const RECOVERY_WINDOW_MS = 60 * 60 * 1000;
|
|
6
7
|
export const createNoopRestartPolicy = () => ({
|
|
7
8
|
injectPostStartMessage() {
|
|
@@ -9,7 +10,7 @@ export const createNoopRestartPolicy = () => ({
|
|
|
9
10
|
},
|
|
10
11
|
markUserStopped() { },
|
|
11
12
|
});
|
|
12
|
-
export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getFlags, }) => {
|
|
13
|
+
export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, memoryInjection, readTasks, getFlags, }) => {
|
|
13
14
|
// Runs the user killed via the Stop button. A deliberate stop is otherwise
|
|
14
15
|
// byte-identical to a crash (both end status 'error'), so without this a
|
|
15
16
|
// stop+Restart would inject the "could not recover" handover with stale open
|
|
@@ -33,14 +34,28 @@ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, inser
|
|
|
33
34
|
return false;
|
|
34
35
|
const workers = snapshot.agents.filter((item) => item.role !== 'orchestrator' && item.id !== agentId);
|
|
35
36
|
const tasksContent = readTasks(snapshot.summary.path);
|
|
36
|
-
if (startConfig.resumedSessionId)
|
|
37
|
-
return true;
|
|
38
37
|
// Deliberate stop + restart: start fresh, no crash handover.
|
|
39
38
|
if (wasUserStopped)
|
|
40
39
|
return false;
|
|
40
|
+
if (startConfig.resumedSessionId)
|
|
41
|
+
return true;
|
|
42
|
+
const memoryDigest = buildMemoryDigestSafely({
|
|
43
|
+
contextType: 'recovery',
|
|
44
|
+
memoryInjection,
|
|
45
|
+
workspaceId: workspace.id,
|
|
46
|
+
});
|
|
47
|
+
const injectionIds = logMemoryDigestInjection({
|
|
48
|
+
agentId,
|
|
49
|
+
contextType: 'recovery',
|
|
50
|
+
memoryDigest,
|
|
51
|
+
memoryInjection,
|
|
52
|
+
workspaceId: workspace.id,
|
|
53
|
+
});
|
|
54
|
+
const auditedMemoryDigest = injectionIds ? memoryDigest : null;
|
|
41
55
|
const text = buildRecoverySummary({
|
|
42
56
|
agent,
|
|
43
57
|
allTaskMessages: listMessagesForRecovery(workspace.id, 0),
|
|
58
|
+
memoryDigest: auditedMemoryDigest?.text,
|
|
44
59
|
messages: listMessagesForRecovery(workspace.id, Date.now() - RECOVERY_WINDOW_MS),
|
|
45
60
|
tasksContent,
|
|
46
61
|
workers,
|
|
@@ -54,6 +69,7 @@ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, inser
|
|
|
54
69
|
runId,
|
|
55
70
|
text,
|
|
56
71
|
writeToRun,
|
|
72
|
+
onWriteFailure: () => rollbackMemoryDigestInjection({ injectionIds, memoryInjection }),
|
|
57
73
|
});
|
|
58
74
|
return true;
|
|
59
75
|
},
|
|
@@ -25,7 +25,7 @@ export const dispatchRoutes = [
|
|
|
25
25
|
if (!workspaceId) {
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
28
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
29
29
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
30
30
|
if (url.searchParams.has('status')) {
|
|
31
31
|
sendJson(response, 400, { error: 'Use state instead of status for dispatch filtering' });
|
|
@@ -7,17 +7,17 @@ const readPathParam = (request) => {
|
|
|
7
7
|
};
|
|
8
8
|
export const fsRoutes = [
|
|
9
9
|
route('GET', '/api/fs/browse', async ({ request, response, store }) => {
|
|
10
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
10
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
11
11
|
const body = await browseDirectory(readPathParam(request));
|
|
12
12
|
sendJson(response, body.ok ? 200 : 400, body);
|
|
13
13
|
}),
|
|
14
14
|
route('GET', '/api/fs/probe', async ({ request, response, store }) => {
|
|
15
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
15
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
16
16
|
const body = await probeDirectory(readPathParam(request));
|
|
17
17
|
sendJson(response, 200, body);
|
|
18
18
|
}),
|
|
19
19
|
route('POST', '/api/fs/pick-folder', async ({ pickFolderService, request, response, store }) => {
|
|
20
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
20
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
21
21
|
const body = await pickFolderService();
|
|
22
22
|
sendJson(response, 200, body);
|
|
23
23
|
}),
|
|
@@ -11,7 +11,7 @@ const readPathParam = (request) => {
|
|
|
11
11
|
};
|
|
12
12
|
export const marketplaceRoutes = [
|
|
13
13
|
route('GET', '/api/marketplace/manifest', ({ request, response, store }) => {
|
|
14
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
14
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
15
15
|
const lang = readLanguageParam(request);
|
|
16
16
|
if (!isMarketplaceLanguage(lang)) {
|
|
17
17
|
sendJson(response, 400, { error: 'Invalid or missing lang parameter (expected en|zh)' });
|
|
@@ -29,7 +29,7 @@ export const marketplaceRoutes = [
|
|
|
29
29
|
}
|
|
30
30
|
}),
|
|
31
31
|
route('GET', '/api/marketplace/agent', ({ request, response, store }) => {
|
|
32
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
32
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
33
33
|
const lang = readLanguageParam(request);
|
|
34
34
|
if (!isMarketplaceLanguage(lang)) {
|
|
35
35
|
sendJson(response, 400, { error: 'Invalid or missing lang parameter (expected en|zh)' });
|
|
@@ -6,7 +6,7 @@ export const openWorkspaceRoutes = [
|
|
|
6
6
|
const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
|
|
7
7
|
if (!workspaceId)
|
|
8
8
|
return;
|
|
9
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
9
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
10
10
|
const body = await readJsonBody(request);
|
|
11
11
|
if (!isOpenTargetId(body.target_id)) {
|
|
12
12
|
sendJson(response, 400, { error: 'Unknown open target', target_id: body.target_id });
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { ForbiddenError } from './http-errors.js';
|
|
2
|
+
import { REMOTE_DAEMON_TOKEN_KEY, REMOTE_ENABLED_KEY, REMOTE_GATEWAY_URL_KEY, } from './remote-config-keys.js';
|
|
3
|
+
import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
|
|
4
|
+
import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
|
|
5
|
+
// Remote-access device-management + pairing routes (M4). Two gate classes:
|
|
6
|
+
// - gateUi: the standard local-OR-tunnel gate every equal-authority route uses. A paired phone may
|
|
7
|
+
// reach these (list devices, revoke, status, audit) exactly like the desktop.
|
|
8
|
+
// - gateLocalDesktopOnly: the TRUST ROOT (Authority Model). Pairing begin/confirm/reject can ONLY be
|
|
9
|
+
// driven from the local desktop cookie path; a tunnel-tagged (phone) request is refused with 403.
|
|
10
|
+
// This is a pairing-ceremony invariant, NOT a feature permission — a phone can never self-approve
|
|
11
|
+
// a new device. Defense in depth: these three paths are ALSO hard-denied on the bridge
|
|
12
|
+
// (remote-bridge-routing DENIED pairing matcher), so even a bypassed gate gets Reset there.
|
|
13
|
+
// Standard local-OR-tunnel gate. A request with no secret header falls to the cookie path (a browser);
|
|
14
|
+
// a tunnel-stamped request short-circuits as authorized. Used by the equal-authority endpoints.
|
|
15
|
+
const gateUi = ({ request, store }) => {
|
|
16
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
17
|
+
};
|
|
18
|
+
// TRUST-ROOT gate: local desktop ONLY. A tunnel-tagged request (authorizeRemoteTunnelRequest === true)
|
|
19
|
+
// is a phone; refuse it. We audit the forbidden attempt with the concrete reason BEFORE throwing
|
|
20
|
+
// (HARDEN D0.3): the audit spec lists "被拒请求及原因" as a required audited action, and the route is
|
|
21
|
+
// the only layer that can attribute a 403 here to 'pairing_confirm_forbidden' (the bridge's onEnd
|
|
22
|
+
// audits the loopback request status-agnostically). Then we pass NO tunnel authorizer to the token
|
|
23
|
+
// check, so even the short-circuit can't admit a tunnel request that slipped a cookie too.
|
|
24
|
+
const gateLocalDesktopOnly = ({ request, store }) => {
|
|
25
|
+
if (store.authorizeRemoteTunnelRequest(request)) {
|
|
26
|
+
store.getRemoteAuditStore().enqueue({
|
|
27
|
+
action: 'reject',
|
|
28
|
+
result: 'rejected',
|
|
29
|
+
rejectReason: 'pairing_confirm_forbidden',
|
|
30
|
+
});
|
|
31
|
+
throw new ForbiddenError('device approval is desktop-only');
|
|
32
|
+
}
|
|
33
|
+
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
34
|
+
};
|
|
35
|
+
const isLoggedIn = (store) => (store.settings.getAppState(REMOTE_GATEWAY_URL_KEY)?.value ?? null) !== null &&
|
|
36
|
+
(store.settings.getAppState(REMOTE_DAEMON_TOKEN_KEY)?.value ?? null) !== null;
|
|
37
|
+
const remoteStatus = (store) => {
|
|
38
|
+
const status = store.getRemoteTunnelStatus();
|
|
39
|
+
return {
|
|
40
|
+
enabled: store.settings.getAppState(REMOTE_ENABLED_KEY)?.value === 'true',
|
|
41
|
+
loggedIn: isLoggedIn(store),
|
|
42
|
+
gatewayUrl: store.settings.getAppState(REMOTE_GATEWAY_URL_KEY)?.value ?? null,
|
|
43
|
+
connected: status === 'online',
|
|
44
|
+
// The FULL tunnel state, not just online/offline — the desktop dot was stuck looking "connected or
|
|
45
|
+
// not" and couldn't show connecting / reconnecting / revoked / logged-out. (connecting+reconnecting
|
|
46
|
+
// are the states a flaky link spends real time in; surfacing them is what makes the dot honest.)
|
|
47
|
+
connection: status,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
// RemoteDeviceRecord is already camelCase + metadata-only (no key material). Identity-mapped so the
|
|
51
|
+
// response shape stays pinned to the store's invariant-7 projection.
|
|
52
|
+
const toDeviceView = (record) => ({
|
|
53
|
+
id: record.id,
|
|
54
|
+
name: record.name,
|
|
55
|
+
createdAt: record.createdAt,
|
|
56
|
+
lastActive: record.lastActive,
|
|
57
|
+
revokedAt: record.revokedAt,
|
|
58
|
+
});
|
|
59
|
+
export const remoteRoutes = [
|
|
60
|
+
route('GET', '/api/remote/status', (ctx) => {
|
|
61
|
+
gateUi(ctx);
|
|
62
|
+
sendJson(ctx.response, 200, remoteStatus(ctx.store));
|
|
63
|
+
}),
|
|
64
|
+
route('PUT', '/api/remote/enabled', async (ctx) => {
|
|
65
|
+
const { request, response, store } = ctx;
|
|
66
|
+
const body = await readJsonBody(request);
|
|
67
|
+
const enabled = body.enabled === true;
|
|
68
|
+
// D0.4 — conditional gate: a remote MAY turn the tunnel OFF (self-disconnect) but NEVER ON. So a
|
|
69
|
+
// tunnel-tagged enable:true is the only enabled-route action that is desktop-only.
|
|
70
|
+
if (enabled && store.authorizeRemoteTunnelRequest(request)) {
|
|
71
|
+
throw new ForbiddenError('remote cannot self-enable');
|
|
72
|
+
}
|
|
73
|
+
gateUi(ctx);
|
|
74
|
+
store.setRemoteEnabled(enabled);
|
|
75
|
+
sendJson(response, 200, remoteStatus(store));
|
|
76
|
+
}),
|
|
77
|
+
// ── trust-root pairing set (desktop-only) ──────────────────────────────────────────────────────
|
|
78
|
+
route('POST', '/api/remote/pairings', (ctx) => {
|
|
79
|
+
gateLocalDesktopOnly(ctx);
|
|
80
|
+
if (!isLoggedIn(ctx.store)) {
|
|
81
|
+
sendJson(ctx.response, 503, { error: 'remote not logged in' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const ticket = ctx.store.getRemotePairing().beginPairing();
|
|
86
|
+
sendJson(ctx.response, 200, ticket);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// beginPairing throws if gateway/daemon id vanished between the status read and here.
|
|
90
|
+
sendJson(ctx.response, 503, { error: 'remote not logged in' });
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
route('GET', '/api/remote/pairings/pending', (ctx) => {
|
|
94
|
+
// HARDEN minor: desktop-only. The confirm dialog backs this; a phone has no use for another
|
|
95
|
+
// device's in-flight SAS, and exposing it would needlessly widen the secret surface (invariant 7).
|
|
96
|
+
gateLocalDesktopOnly(ctx);
|
|
97
|
+
sendJson(ctx.response, 200, ctx.store.getRemotePairing().listPending());
|
|
98
|
+
}),
|
|
99
|
+
route('POST', '/api/remote/pairings/:pairingId/confirm', async (ctx) => {
|
|
100
|
+
const { params, request, response, store } = ctx;
|
|
101
|
+
const pairingId = getRequiredParam(response, params, 'pairingId', 'Pairing id is required');
|
|
102
|
+
if (!pairingId)
|
|
103
|
+
return;
|
|
104
|
+
gateLocalDesktopOnly(ctx);
|
|
105
|
+
const body = await readJsonBody(request).catch(() => ({}));
|
|
106
|
+
// THE trust-root action (D3): confirmRemotePairing drives the engine's local insert (the only
|
|
107
|
+
// caller of deviceStore.insert) AND the gateway device-row registration + the `confirmed` signal
|
|
108
|
+
// to the phone, in that order. A gateway-POST failure (or a missing boundJti) rejects here: the
|
|
109
|
+
// local row exists but the phone is NOT told OK, so it can re-scan. Surface 502 so the desktop
|
|
110
|
+
// operator sees the pairing didn't complete end-to-end rather than a false success.
|
|
111
|
+
let record;
|
|
112
|
+
try {
|
|
113
|
+
record = await store.confirmRemotePairing(pairingId, body.name === undefined ? undefined : body.name);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
sendJson(response, 502, { error: 'gateway registration failed; rescan to retry' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!record) {
|
|
120
|
+
// Unknown / expired / wrong-state pairing. Nothing persisted.
|
|
121
|
+
sendJson(response, 404, { error: 'pairing not found or no longer confirmable' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// The newly-persisted device is live in the provider now. Reconcile the tunnel so a freshly
|
|
125
|
+
// enabled remote starts connecting (no-op if already online / disabled).
|
|
126
|
+
store.setRemoteEnabled(store.settings.getAppState(REMOTE_ENABLED_KEY)?.value === 'true');
|
|
127
|
+
sendJson(response, 200, { device: toDeviceView(record) });
|
|
128
|
+
}),
|
|
129
|
+
route('POST', '/api/remote/pairings/:pairingId/reject', (ctx) => {
|
|
130
|
+
const { params, response, store } = ctx;
|
|
131
|
+
const pairingId = getRequiredParam(response, params, 'pairingId', 'Pairing id is required');
|
|
132
|
+
if (!pairingId)
|
|
133
|
+
return;
|
|
134
|
+
gateLocalDesktopOnly(ctx);
|
|
135
|
+
store.getRemotePairing().rejectPairing(pairingId, 'user_rejected');
|
|
136
|
+
response.statusCode = 204;
|
|
137
|
+
response.end();
|
|
138
|
+
}),
|
|
139
|
+
// ── equal-authority device management ──────────────────────────────────────────────────────────
|
|
140
|
+
route('GET', '/api/remote/devices', (ctx) => {
|
|
141
|
+
gateUi(ctx);
|
|
142
|
+
const includeRevoked = new URL(ctx.request.url ?? '', 'http://x').searchParams.has('includeRevoked');
|
|
143
|
+
sendJson(ctx.response, 200, ctx.store.getRemoteDeviceStore().list(includeRevoked).map(toDeviceView));
|
|
144
|
+
}),
|
|
145
|
+
route('POST', '/api/remote/devices/:deviceId/revoke', (ctx) => {
|
|
146
|
+
const { params, response, store } = ctx;
|
|
147
|
+
const deviceId = getRequiredParam(response, params, 'deviceId', 'Device id is required');
|
|
148
|
+
if (!deviceId)
|
|
149
|
+
return;
|
|
150
|
+
gateUi(ctx);
|
|
151
|
+
// Revocation closed loop (§6 orchestrator in the runtime store): provider drop + tunnel close +
|
|
152
|
+
// audit. Equal-authority: a phone may revoke any device, including itself.
|
|
153
|
+
store.revokeRemoteDevice(deviceId);
|
|
154
|
+
response.statusCode = 204;
|
|
155
|
+
response.end();
|
|
156
|
+
}),
|
|
157
|
+
route('GET', '/api/remote/audit', (ctx) => {
|
|
158
|
+
gateUi(ctx);
|
|
159
|
+
const url = new URL(ctx.request.url ?? '', 'http://x');
|
|
160
|
+
const rawLimit = Number(url.searchParams.get('limit'));
|
|
161
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 500) : 100;
|
|
162
|
+
const deviceId = url.searchParams.get('deviceId');
|
|
163
|
+
const audit = ctx.store.getRemoteAuditStore();
|
|
164
|
+
sendJson(ctx.response, 200, deviceId ? audit.listForDevice(deviceId, limit) : audit.list(limit));
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
@@ -8,7 +8,7 @@ export const runtimeRoutes = [
|
|
|
8
8
|
if (!workspaceId) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
11
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
12
12
|
sendJson(response, 200, store.listTerminalRuns(workspaceId));
|
|
13
13
|
}),
|
|
14
14
|
route('POST', '/api/workspaces/:workspaceId/shell/start', async ({ params, request, response, store }) => {
|
|
@@ -16,7 +16,7 @@ export const runtimeRoutes = [
|
|
|
16
16
|
if (!workspaceId) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
19
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
20
20
|
const run = await store.startWorkspaceShell(workspaceId);
|
|
21
21
|
const summary = store
|
|
22
22
|
.listTerminalRuns(workspaceId)
|
|
@@ -35,7 +35,7 @@ export const runtimeRoutes = [
|
|
|
35
35
|
if (!workspaceId || !runId) {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
38
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
39
39
|
if (!store.closeWorkspaceShell(workspaceId, runId)) {
|
|
40
40
|
sendJson(response, 404, { error: 'Shell run not found' });
|
|
41
41
|
return;
|
|
@@ -49,7 +49,7 @@ export const runtimeRoutes = [
|
|
|
49
49
|
if (!workspaceId || !agentId) {
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
52
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
53
53
|
const body = await readJsonBody(request);
|
|
54
54
|
store.configureAgentLaunch(workspaceId, agentId, {
|
|
55
55
|
command: body.command,
|
|
@@ -64,7 +64,7 @@ export const runtimeRoutes = [
|
|
|
64
64
|
if (!runId) {
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
67
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
68
68
|
store.stopAgentRun(runId);
|
|
69
69
|
sendJson(response, 202, { ok: true });
|
|
70
70
|
}),
|
|
@@ -73,7 +73,7 @@ export const runtimeRoutes = [
|
|
|
73
73
|
if (!runId) {
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
76
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
77
77
|
sendJson(response, 200, serializeLiveAgentRun(store.getLiveRun(runId)));
|
|
78
78
|
}),
|
|
79
79
|
];
|
|
@@ -85,15 +85,15 @@ const readRoleTemplateBody = async (request) => {
|
|
|
85
85
|
};
|
|
86
86
|
export const settingsRoutes = [
|
|
87
87
|
route('GET', '/api/settings/command-presets', ({ request, response, store }) => {
|
|
88
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
88
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
89
89
|
sendJson(response, 200, store.settings.listCommandPresets().map(serializeCommandPreset));
|
|
90
90
|
}),
|
|
91
91
|
route('POST', '/api/settings/command-presets', async ({ request, response, store }) => {
|
|
92
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
92
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
93
93
|
sendJson(response, 201, serializeCommandPreset(store.settings.createCommandPreset(await readCommandPresetBody(request))));
|
|
94
94
|
}),
|
|
95
95
|
route('PATCH', '/api/settings/command-presets/:presetId', async ({ params, request, response, store }) => {
|
|
96
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
96
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
97
97
|
const presetId = getRequiredParam(response, params, 'presetId', 'Preset id is required');
|
|
98
98
|
if (!presetId)
|
|
99
99
|
return;
|
|
@@ -104,7 +104,7 @@ export const settingsRoutes = [
|
|
|
104
104
|
sendJson(response, 200, serializeCommandPreset(store.settings.updateCommandPreset(presetId, next)));
|
|
105
105
|
}),
|
|
106
106
|
route('DELETE', '/api/settings/command-presets/:presetId', ({ params, request, response, store }) => {
|
|
107
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
107
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
108
108
|
const presetId = getRequiredParam(response, params, 'presetId', 'Preset id is required');
|
|
109
109
|
if (!presetId)
|
|
110
110
|
return;
|
|
@@ -113,15 +113,15 @@ export const settingsRoutes = [
|
|
|
113
113
|
response.end();
|
|
114
114
|
}),
|
|
115
115
|
route('GET', '/api/settings/role-templates', ({ request, response, store }) => {
|
|
116
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
116
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
117
117
|
sendJson(response, 200, store.settings.listRoleTemplates().map(serializeRoleTemplate));
|
|
118
118
|
}),
|
|
119
119
|
route('POST', '/api/settings/role-templates', async ({ request, response, store }) => {
|
|
120
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
120
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
121
121
|
sendJson(response, 201, serializeRoleTemplate(store.settings.createRoleTemplate(await readRoleTemplateBody(request))));
|
|
122
122
|
}),
|
|
123
123
|
route('PATCH', '/api/settings/role-templates/:templateId', async ({ params, request, response, store }) => {
|
|
124
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
124
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
125
125
|
const templateId = getRequiredParam(response, params, 'templateId', 'Template id is required');
|
|
126
126
|
if (!templateId)
|
|
127
127
|
return;
|
|
@@ -134,7 +134,7 @@ export const settingsRoutes = [
|
|
|
134
134
|
sendJson(response, 200, serializeRoleTemplate(store.settings.updateRoleTemplate(templateId, next)));
|
|
135
135
|
}),
|
|
136
136
|
route('DELETE', '/api/settings/role-templates/:templateId', ({ params, request, response, store }) => {
|
|
137
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
137
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
138
138
|
const templateId = getRequiredParam(response, params, 'templateId', 'Template id is required');
|
|
139
139
|
if (!templateId)
|
|
140
140
|
return;
|
|
@@ -143,14 +143,14 @@ export const settingsRoutes = [
|
|
|
143
143
|
response.end();
|
|
144
144
|
}),
|
|
145
145
|
route('GET', '/api/settings/app-state/:key', ({ params, request, response, store }) => {
|
|
146
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
146
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
147
147
|
const key = getRequiredParam(response, params, 'key', 'App state key is required');
|
|
148
148
|
if (!key)
|
|
149
149
|
return;
|
|
150
150
|
sendJson(response, 200, store.settings.getAppState(key) ?? { key, value: null });
|
|
151
151
|
}),
|
|
152
152
|
route('PUT', '/api/settings/app-state/:key', async ({ params, request, response, store }) => {
|
|
153
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
153
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
154
154
|
const key = getRequiredParam(response, params, 'key', 'App state key is required');
|
|
155
155
|
if (!key)
|
|
156
156
|
return;
|
|
@@ -160,12 +160,12 @@ export const settingsRoutes = [
|
|
|
160
160
|
response.end();
|
|
161
161
|
}),
|
|
162
162
|
route('GET', '/api/settings/workflow-cli-policy', ({ request, response, store }) => {
|
|
163
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
163
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
164
164
|
const policy = readWorkflowCliPolicy(store.settings.getAppState(WORKFLOW_CLI_POLICY_KEY)?.value ?? null);
|
|
165
165
|
sendJson(response, 200, { ...policy, supported: [...CANONICAL_WORKFLOW_CLIS] });
|
|
166
166
|
}),
|
|
167
167
|
route('PUT', '/api/settings/workflow-cli-policy', async ({ request, response, store }) => {
|
|
168
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
168
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
169
169
|
const body = await readJsonBody(request);
|
|
170
170
|
// Strict validation: a bad payload is rejected (400) rather than persisted.
|
|
171
171
|
const clean = (() => {
|
|
@@ -181,12 +181,12 @@ export const settingsRoutes = [
|
|
|
181
181
|
sendJson(response, 200, { ...clean, supported: [...CANONICAL_WORKFLOW_CLIS] });
|
|
182
182
|
}),
|
|
183
183
|
route('GET', '/api/settings/workflow-feature', ({ request, response, store }) => {
|
|
184
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
184
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
185
185
|
const enabled = readWorkflowEnabled(store.settings.getAppState(WORKFLOW_ENABLED_KEY)?.value ?? null);
|
|
186
186
|
sendJson(response, 200, { enabled });
|
|
187
187
|
}),
|
|
188
188
|
route('PUT', '/api/settings/workflow-feature', async ({ request, response, store }) => {
|
|
189
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
189
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
190
190
|
const body = await readJsonBody(request);
|
|
191
191
|
if (typeof body.enabled !== 'boolean') {
|
|
192
192
|
throw new BadRequestError('workflow-feature requires { enabled: boolean }');
|
|
@@ -196,12 +196,12 @@ export const settingsRoutes = [
|
|
|
196
196
|
sendJson(response, 200, { enabled: body.enabled });
|
|
197
197
|
}),
|
|
198
198
|
route('GET', '/api/settings/team-autostaff', ({ request, response, store }) => {
|
|
199
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
199
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
200
200
|
const enabled = readAutostaffEnabled(store.settings.getAppState(AUTOSTAFF_ENABLED_KEY)?.value ?? null);
|
|
201
201
|
sendJson(response, 200, { enabled });
|
|
202
202
|
}),
|
|
203
203
|
route('PUT', '/api/settings/team-autostaff', async ({ request, response, store }) => {
|
|
204
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
204
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
205
205
|
const body = await readJsonBody(request);
|
|
206
206
|
if (typeof body.enabled !== 'boolean') {
|
|
207
207
|
throw new BadRequestError('team-autostaff requires { enabled: boolean }');
|
|
@@ -29,7 +29,7 @@ export const taskRoutes = [
|
|
|
29
29
|
if (!workspaceId) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
32
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
33
33
|
const workspace = store.getWorkspaceSnapshot(workspaceId);
|
|
34
34
|
sendJson(response, 200, { content: tasksFileService.readTasks(workspace.summary.path) });
|
|
35
35
|
}),
|
|
@@ -38,7 +38,7 @@ export const taskRoutes = [
|
|
|
38
38
|
if (!workspaceId) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
requireUiTokenFromRequest(request, store.validateUiToken);
|
|
41
|
+
requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
|
|
42
42
|
const body = await readJsonBody(request);
|
|
43
43
|
const workspace = store.getWorkspaceSnapshot(workspaceId);
|
|
44
44
|
tasksFileService.writeTasks(workspace.summary.path, body.content);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { isMemoryKind, MEMORY_BODY_MAX_CHARS, MEMORY_KINDS, MEMORY_QUERY_MAX_CHARS, MEMORY_SEARCH_MAX_LIMIT, MEMORY_TAG_MAX_CHARS, MEMORY_TAG_MAX_COUNT, } from '../shared/team-memory.js';
|
|
2
|
+
import { BadRequestError } from './http-errors.js';
|
|
3
|
+
import { readJsonBody, route, sendJson } from './route-helpers.js';
|
|
4
|
+
import { authenticateCliAgent, requireCommandForRole } from './team-authz.js';
|
|
5
|
+
import { serializeMemoryEntry, serializeMemorySearchResult, } from './team-memory-http-serializers.js';
|
|
6
|
+
import { MemoryEntryStatusError } from './team-memory-store.js';
|
|
7
|
+
const requireNonEmptyString = (value, field) => {
|
|
8
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
9
|
+
throw new BadRequestError(`Missing ${field}`);
|
|
10
|
+
}
|
|
11
|
+
return value.trim();
|
|
12
|
+
};
|
|
13
|
+
const requireMemoryBody = (value) => {
|
|
14
|
+
const body = requireNonEmptyString(value, 'body');
|
|
15
|
+
if ([...body].length > MEMORY_BODY_MAX_CHARS) {
|
|
16
|
+
throw new BadRequestError(`body must be ${MEMORY_BODY_MAX_CHARS} characters or fewer`);
|
|
17
|
+
}
|
|
18
|
+
return body;
|
|
19
|
+
};
|
|
20
|
+
const requireMemoryQuery = (value) => {
|
|
21
|
+
const query = requireNonEmptyString(value, 'query');
|
|
22
|
+
if ([...query].length > MEMORY_QUERY_MAX_CHARS) {
|
|
23
|
+
throw new BadRequestError(`query must be ${MEMORY_QUERY_MAX_CHARS} characters or fewer`);
|
|
24
|
+
}
|
|
25
|
+
return query;
|
|
26
|
+
};
|
|
27
|
+
const parseLimit = (value) => {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
31
|
+
throw new BadRequestError('limit must be a non-negative integer');
|
|
32
|
+
}
|
|
33
|
+
return Math.min(value, MEMORY_SEARCH_MAX_LIMIT);
|
|
34
|
+
};
|
|
35
|
+
const parseKind = (value) => {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return 'fact';
|
|
38
|
+
if (!isMemoryKind(value)) {
|
|
39
|
+
throw new BadRequestError(`kind must be one of: ${MEMORY_KINDS.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const parseTags = (value) => {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
if (!Array.isArray(value)) {
|
|
47
|
+
throw new BadRequestError('tags must be an array of strings');
|
|
48
|
+
}
|
|
49
|
+
if (value.length > MEMORY_TAG_MAX_COUNT) {
|
|
50
|
+
throw new BadRequestError(`tags must contain ${MEMORY_TAG_MAX_COUNT} items or fewer`);
|
|
51
|
+
}
|
|
52
|
+
const tags = [];
|
|
53
|
+
for (const item of value) {
|
|
54
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
55
|
+
throw new BadRequestError('tags must be non-empty strings');
|
|
56
|
+
}
|
|
57
|
+
const tag = item.trim();
|
|
58
|
+
if ([...tag].length > MEMORY_TAG_MAX_CHARS) {
|
|
59
|
+
throw new BadRequestError(`tags must be ${MEMORY_TAG_MAX_CHARS} characters or fewer`);
|
|
60
|
+
}
|
|
61
|
+
if (!tags.includes(tag))
|
|
62
|
+
tags.push(tag);
|
|
63
|
+
}
|
|
64
|
+
return tags;
|
|
65
|
+
};
|
|
66
|
+
const authenticateMemoryRequest = (body, store) => {
|
|
67
|
+
const workspaceId = requireNonEmptyString(body.project_id, 'project_id');
|
|
68
|
+
const fromAgentId = requireNonEmptyString(body.from_agent_id, 'from_agent_id');
|
|
69
|
+
const token = typeof body.token === 'string' ? body.token : undefined;
|
|
70
|
+
const agent = authenticateCliAgent({
|
|
71
|
+
fromAgentId,
|
|
72
|
+
getAgent: store.getAgent,
|
|
73
|
+
token,
|
|
74
|
+
validateToken: store.validateAgentToken,
|
|
75
|
+
workspaceId,
|
|
76
|
+
});
|
|
77
|
+
return { agent, workspaceId };
|
|
78
|
+
};
|
|
79
|
+
export const teamMemoryRoutes = [
|
|
80
|
+
route('POST', '/api/team/memory/add', async ({ request, response, store }) => {
|
|
81
|
+
const body = await readJsonBody(request);
|
|
82
|
+
const { agent, workspaceId } = authenticateMemoryRequest(body, store);
|
|
83
|
+
requireCommandForRole(agent, 'memory_add');
|
|
84
|
+
const memory = store.addMemoryEntry({
|
|
85
|
+
actor: {
|
|
86
|
+
id: agent.id,
|
|
87
|
+
name: agent.name,
|
|
88
|
+
role: agent.role,
|
|
89
|
+
},
|
|
90
|
+
body: requireMemoryBody(body.body),
|
|
91
|
+
kind: parseKind(body.kind),
|
|
92
|
+
tags: parseTags(body.tags),
|
|
93
|
+
workspaceId,
|
|
94
|
+
});
|
|
95
|
+
sendJson(response, 200, {
|
|
96
|
+
memory: serializeMemoryEntry(memory),
|
|
97
|
+
ok: true,
|
|
98
|
+
});
|
|
99
|
+
}),
|
|
100
|
+
route('POST', '/api/team/memory/show', async ({ request, response, store }) => {
|
|
101
|
+
const body = await readJsonBody(request);
|
|
102
|
+
const { agent, workspaceId } = authenticateMemoryRequest(body, store);
|
|
103
|
+
requireCommandForRole(agent, 'memory_show');
|
|
104
|
+
const memoryId = requireNonEmptyString(body.memory_id, 'memory_id');
|
|
105
|
+
const memory = store.getMemoryEntry(workspaceId, memoryId);
|
|
106
|
+
if (!memory) {
|
|
107
|
+
sendJson(response, 404, { error: `Memory entry not found: ${memoryId}` });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
sendJson(response, 200, {
|
|
111
|
+
memory: serializeMemoryEntry(memory),
|
|
112
|
+
ok: true,
|
|
113
|
+
});
|
|
114
|
+
}),
|
|
115
|
+
route('POST', '/api/team/memory/search', async ({ request, response, store }) => {
|
|
116
|
+
const body = await readJsonBody(request);
|
|
117
|
+
const { agent, workspaceId } = authenticateMemoryRequest(body, store);
|
|
118
|
+
requireCommandForRole(agent, 'memory_search');
|
|
119
|
+
const limit = parseLimit(body.limit);
|
|
120
|
+
const results = store.searchMemoryEntries(workspaceId, requireMemoryQuery(body.query), {
|
|
121
|
+
...(limit !== undefined ? { limit } : {}),
|
|
122
|
+
});
|
|
123
|
+
sendJson(response, 200, {
|
|
124
|
+
ok: true,
|
|
125
|
+
results: results.map(serializeMemorySearchResult),
|
|
126
|
+
});
|
|
127
|
+
}),
|
|
128
|
+
route('POST', '/api/team/memory/forget', async ({ request, response, store }) => {
|
|
129
|
+
const body = await readJsonBody(request);
|
|
130
|
+
const { agent, workspaceId } = authenticateMemoryRequest(body, store);
|
|
131
|
+
requireCommandForRole(agent, 'memory_forget');
|
|
132
|
+
const memoryId = requireNonEmptyString(body.memory_id, 'memory_id');
|
|
133
|
+
const memory = store.getMemoryEntry(workspaceId, memoryId);
|
|
134
|
+
if (!memory) {
|
|
135
|
+
sendJson(response, 404, { error: `Memory entry not found: ${memoryId}` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
sendJson(response, 200, {
|
|
140
|
+
memory: serializeMemoryEntry(store.archiveMemoryEntry(workspaceId, memoryId)),
|
|
141
|
+
ok: true,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof MemoryEntryStatusError) {
|
|
146
|
+
sendJson(response, 409, {
|
|
147
|
+
error: `Memory entry has status ${error.actualStatus}; expected active`,
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}),
|
|
154
|
+
];
|