@zhanguym/chatroom 1.0.0 → 1.1.0-beta.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.
Files changed (165) hide show
  1. package/dist/app/application.d.ts +0 -1
  2. package/dist/app/application.js +8 -7
  3. package/dist/app/event-bus.d.ts +13 -10
  4. package/dist/app/external-access-registry.d.ts +1 -0
  5. package/dist/app/external-access-registry.js +3 -0
  6. package/dist/app/git-access.d.ts +50 -0
  7. package/dist/app/git-access.js +2 -0
  8. package/dist/app/service-registry.d.ts +11 -0
  9. package/dist/app/service-registry.js +17 -0
  10. package/dist/app/workspace-access.d.ts +24 -0
  11. package/dist/app/workspace-access.js +2 -0
  12. package/dist/auth/ingress-policy.d.ts +2 -0
  13. package/dist/auth/ingress-policy.js +14 -2
  14. package/dist/auth/passkey-service.js +1 -1
  15. package/dist/cli/run-cli.js +1 -1
  16. package/dist/core/fs/safe-directory.d.ts +9 -0
  17. package/dist/core/fs/safe-directory.js +129 -0
  18. package/dist/core/operations/repository.d.ts +2 -0
  19. package/dist/core/runtime/line-process.d.ts +19 -0
  20. package/dist/core/runtime/line-process.js +51 -0
  21. package/dist/infrastructure/database/operation-repository.js +8 -2
  22. package/dist/infrastructure/database/schema.d.ts +1 -1
  23. package/dist/infrastructure/database/schema.js +62 -0
  24. package/dist/infrastructure/http/http-server.js +1 -1
  25. package/dist/infrastructure/logging/system-log.js +2 -1
  26. package/dist/native/linux/chatroom-computer-helper +0 -0
  27. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  28. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  29. package/dist/operations/operation-log.js +16 -4
  30. package/dist/plugins/agent/async-queue.d.ts +10 -0
  31. package/dist/plugins/agent/async-queue.js +49 -0
  32. package/dist/plugins/agent/mcp.d.ts +3 -0
  33. package/dist/plugins/agent/mcp.js +502 -0
  34. package/dist/plugins/agent/plugin.d.ts +4 -0
  35. package/dist/plugins/agent/plugin.js +35 -0
  36. package/dist/plugins/agent/provider.d.ts +132 -0
  37. package/dist/plugins/agent/providers/codex/mapping.d.ts +56 -0
  38. package/dist/plugins/agent/providers/codex/mapping.js +427 -0
  39. package/dist/plugins/agent/providers/codex/provider.d.ts +84 -0
  40. package/dist/plugins/agent/providers/codex/provider.js +828 -0
  41. package/dist/plugins/agent/providers/codex/rpc.d.ts +40 -0
  42. package/dist/plugins/agent/providers/codex/rpc.js +227 -0
  43. package/dist/plugins/agent/providers/common.d.ts +11 -0
  44. package/dist/plugins/agent/providers/common.js +75 -0
  45. package/dist/plugins/agent/providers/index.d.ts +3 -0
  46. package/dist/plugins/agent/providers/index.js +4 -0
  47. package/dist/plugins/agent/repository.d.ts +21 -0
  48. package/dist/plugins/agent/repository.js +226 -0
  49. package/dist/plugins/agent/runtime-config.d.ts +5 -0
  50. package/dist/plugins/agent/runtime-config.js +60 -0
  51. package/dist/plugins/agent/runtime-files.d.ts +8 -0
  52. package/dist/plugins/agent/runtime-files.js +170 -0
  53. package/dist/plugins/agent/runtime-state.d.ts +17 -0
  54. package/dist/plugins/agent/runtime-state.js +133 -0
  55. package/dist/plugins/agent/runtime.d.ts +60 -0
  56. package/dist/plugins/agent/runtime.js +928 -0
  57. package/dist/plugins/agent/store.d.ts +17 -0
  58. package/dist/plugins/agent/store.js +1 -0
  59. package/dist/plugins/agent/types.d.ts +320 -0
  60. package/dist/plugins/agent/types.js +13 -0
  61. package/dist/plugins/cloud/plugin.d.ts +1 -1
  62. package/dist/plugins/cloud/plugin.js +3 -3
  63. package/dist/plugins/cloud/tunnel-client.js +6 -7
  64. package/dist/plugins/computer/computer-native-backend.js +9 -1
  65. package/dist/plugins/computer/computer-native-host.js +21 -5
  66. package/dist/plugins/computer/computer-schemas.d.ts +7 -7
  67. package/dist/plugins/computer/computer-service.d.ts +3 -0
  68. package/dist/plugins/computer/computer-service.js +33 -1
  69. package/dist/plugins/computer/plugin.d.ts +1 -1
  70. package/dist/plugins/computer/plugin.js +1 -1
  71. package/dist/plugins/computer/types.d.ts +1 -0
  72. package/dist/plugins/git/git-service.d.ts +3 -2
  73. package/dist/plugins/git/git-service.js +37 -1
  74. package/dist/plugins/git/plugin.d.ts +0 -2
  75. package/dist/plugins/git/plugin.js +3 -3
  76. package/dist/plugins/process/plugin.d.ts +1 -1
  77. package/dist/plugins/process/plugin.js +3 -3
  78. package/dist/plugins/process/process-supervisor.d.ts +1 -0
  79. package/dist/plugins/process/process-supervisor.js +15 -4
  80. package/dist/plugins/types.d.ts +1 -11
  81. package/dist/plugins/types.js +1 -17
  82. package/dist/plugins/web/api-types.d.ts +20 -5
  83. package/dist/plugins/web/http/agent-api-router.d.ts +3 -0
  84. package/dist/plugins/web/http/agent-api-router.js +254 -0
  85. package/dist/plugins/web/http/api-router.js +9 -0
  86. package/dist/plugins/web/http/computer-api-router.js +21 -6
  87. package/dist/plugins/web/http/process-api-router.js +3 -0
  88. package/dist/plugins/web/http/workspace-api-router.js +198 -8
  89. package/dist/plugins/web/plugin.d.ts +1 -1
  90. package/dist/plugins/web/plugin.js +5 -4
  91. package/dist/plugins/web/runtime.d.ts +6 -3
  92. package/dist/plugins/web/runtime.js +6 -1
  93. package/dist/plugins/workspace/mcp.d.ts +1 -1
  94. package/dist/plugins/workspace/mcp.js +125 -1
  95. package/dist/plugins/workspace/metadata.d.ts +1 -1
  96. package/dist/plugins/workspace/plugin.d.ts +1 -1
  97. package/dist/plugins/workspace/plugin.js +9 -3
  98. package/dist/plugins/workspace/types.d.ts +1 -18
  99. package/dist/plugins/workspace/workspace-fs.d.ts +19 -3
  100. package/dist/plugins/workspace/workspace-fs.js +112 -120
  101. package/dist/plugins/workspace/workspace-root-policy.d.ts +9 -0
  102. package/dist/plugins/workspace/workspace-root-policy.js +56 -0
  103. package/dist/plugins/workspace/workspace-service.d.ts +13 -6
  104. package/dist/plugins/workspace/workspace-service.js +57 -49
  105. package/dist/web/assets/AgentView-BGLIBoxF.css +1 -0
  106. package/dist/web/assets/AgentView-BwL3S7sb.js +4 -0
  107. package/dist/web/assets/CloudView-DH4sOXJr.js +1 -0
  108. package/dist/web/assets/{CloudView-CHjVbp-5.css → CloudView-DVVvwuWW.css} +1 -1
  109. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-BaC1eGzG.js +3 -0
  110. package/dist/web/assets/ComputerView-BJSjCxFr.js +1 -0
  111. package/dist/web/assets/ComputerView-DWzfSYJ7.css +1 -0
  112. package/dist/web/assets/MarkdownContent-AdAmmDn5.js +16 -0
  113. package/dist/web/assets/MarkdownContent-B_5zTG8c.css +1 -0
  114. package/dist/web/assets/McpToolsView-CPaWUG9h.js +1 -0
  115. package/dist/web/assets/McpToolsView-DjxPbTHj.css +1 -0
  116. package/dist/web/assets/OperationsView-BjTY2lIh.css +1 -0
  117. package/dist/web/assets/OperationsView-D2FtLkHn.js +4 -0
  118. package/dist/web/assets/ProcessesView-BHOsaM2H.js +4 -0
  119. package/dist/web/assets/ProcessesView-Ce6iVwjA.css +1 -0
  120. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-iW1SYlX_.js +1 -0
  121. package/dist/web/assets/SystemLogsView-By5ijObd.js +1 -0
  122. package/dist/web/assets/SystemLogsView-CofP4Jvi.css +1 -0
  123. package/dist/web/assets/VSelect-CLnwhyWJ.js +1 -0
  124. package/dist/web/assets/VSpacer-dDRRKhUn.js +1 -0
  125. package/dist/web/assets/VSwitch-CGzxwZuj.js +1 -0
  126. package/dist/web/assets/VTabs-7aSwjrKs.js +1 -0
  127. package/dist/web/assets/VTextarea-D1_2FwMe.js +1 -0
  128. package/dist/web/assets/VTextarea-xiMOHEu-.css +1 -0
  129. package/dist/web/assets/WorkspaceDownloadPreview-BjuqUdB9.js +1 -0
  130. package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +1 -0
  131. package/dist/web/assets/WorkspaceMediaPreview-D-igEnka.css +1 -0
  132. package/dist/web/assets/WorkspaceMediaPreview-LwXeVOY2.js +3 -0
  133. package/dist/web/assets/WorkspacesView-ClA6xcjn.css +1 -0
  134. package/dist/web/assets/WorkspacesView-DHl28eWf.js +2 -0
  135. package/dist/web/assets/entity-refresh-queue-wQii8Dl5.js +1 -0
  136. package/dist/web/assets/index-BzRlodie.js +19 -0
  137. package/dist/web/assets/index-CZ-4tJlF.css +1 -0
  138. package/dist/web/assets/{syntax-highlight-B5ArnyrC.js → syntax-highlight-C_fuMcwC.js} +1 -1
  139. package/dist/web/assets/useMasterDetailFocus-BQAutDO9.js +1 -0
  140. package/dist/web/assets/useOperationLabels-C-zgM_ET.js +1 -0
  141. package/dist/web/index.html +2 -2
  142. package/package.json +4 -2
  143. package/dist/plugins/git/types.d.ts +0 -32
  144. package/dist/web/assets/CloudView-CvX0dkgu.js +0 -1
  145. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-B5TxGkUW.js +0 -3
  146. package/dist/web/assets/ComputerView-91Z9Cefw.css +0 -1
  147. package/dist/web/assets/ComputerView-OOyEqWml.js +0 -1
  148. package/dist/web/assets/McpToolsView-BTnolyS6.css +0 -1
  149. package/dist/web/assets/McpToolsView-DjXWTKrZ.js +0 -1
  150. package/dist/web/assets/OperationsView-DqTY51Cr.js +0 -4
  151. package/dist/web/assets/ProcessesView-C13BykIa.js +0 -4
  152. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DyOhJmR6.js +0 -1
  153. package/dist/web/assets/SystemLogsView-Ci0u4BTb.js +0 -1
  154. package/dist/web/assets/VSelect-DrRpKyYI.js +0 -1
  155. package/dist/web/assets/VSpacer-C3wAY3F0.js +0 -1
  156. package/dist/web/assets/VSwitch-CxusR4iO.js +0 -1
  157. package/dist/web/assets/VTabs-qBcuuXh3.js +0 -1
  158. package/dist/web/assets/WorkspacesView-BzQ0aH74.css +0 -1
  159. package/dist/web/assets/WorkspacesView-CijIvfsp.js +0 -2
  160. package/dist/web/assets/index-BegKQ3Yd.js +0 -19
  161. package/dist/web/assets/index-CkjqXRSj.css +0 -1
  162. package/dist/web/assets/useOperationLabels-BDjzGDkT.js +0 -1
  163. /package/dist/{plugins/process → core/runtime}/head-tail-buffer.d.ts +0 -0
  164. /package/dist/{plugins/process → core/runtime}/head-tail-buffer.js +0 -0
  165. /package/dist/plugins/{git/types.js → agent/provider.js} +0 -0
@@ -10,7 +10,6 @@ export interface ApplicationComponents {
10
10
  eventBus: RuntimeEventBus;
11
11
  operations: OperationLog;
12
12
  plugins: PluginManager;
13
- application: import("#plugins/web/runtime").WebRuntime;
14
13
  processes: import("#plugins/process/process-supervisor").ProcessSupervisor;
15
14
  cloud: import("#plugins/cloud/controller").CloudController;
16
15
  computer: import("#plugins/computer/computer-service").ComputerService;
@@ -9,13 +9,14 @@ import { OperationLog } from "#operations/operation-log";
9
9
  import { AuthService } from "#auth/auth-service";
10
10
  import { PasskeyService } from "#auth/passkey-service";
11
11
  import { ExternalAccessRegistry } from "./external-access-registry.js";
12
- import { ServiceRegistry } from "#plugins/types";
12
+ import { ServiceRegistry } from "#app/service-registry";
13
13
  import { PluginManager } from "#plugins/plugin-manager";
14
14
  import { createWorkspacePlugin } from "#plugins/workspace/plugin";
15
15
  import { createGitPlugin } from "#plugins/git/plugin";
16
- import { createProcessPlugin, ProcessService } from "#plugins/process/plugin";
17
- import { createCloudPlugin, CloudService } from "#plugins/cloud/plugin";
16
+ import { createProcessPlugin, ProcessServiceToken, } from "#plugins/process/plugin";
17
+ import { createCloudPlugin, CloudServiceToken } from "#plugins/cloud/plugin";
18
18
  import { createComputerPlugin, ComputerServiceToken, } from "#plugins/computer/plugin";
19
+ import { createAgentPlugin } from "#plugins/agent/plugin";
19
20
  import { createWebPlugin, WebServiceToken } from "#plugins/web/plugin";
20
21
  import { createChatRoomMcpHandler } from "#mcp/server/create-mcp-server";
21
22
  import { McpToolControl } from "#mcp/server/tool-control";
@@ -41,17 +42,18 @@ export async function createApplication(config, logs) {
41
42
  services,
42
43
  logs,
43
44
  }, [
44
- createWorkspacePlugin(),
45
45
  createGitPlugin(),
46
+ createWorkspacePlugin(),
46
47
  createProcessPlugin(),
47
48
  createComputerPlugin(),
48
49
  createCloudPlugin(),
50
+ createAgentPlugin(),
49
51
  createWebPlugin(),
50
52
  ]);
51
53
  await plugins.start();
52
54
  const web = services.require(WebServiceToken);
53
- const processes = services.require(ProcessService);
54
- const cloud = services.require(CloudService);
55
+ const processes = services.require(ProcessServiceToken);
56
+ const cloud = services.require(CloudServiceToken);
55
57
  const computer = services.require(ComputerServiceToken);
56
58
  const mcp = createChatRoomMcpHandler(plugins);
57
59
  const http = new HttpServer(config, web.application, eventBus, auth, passkeys, mcp, externalAccess, cloud, logs);
@@ -60,7 +62,6 @@ export async function createApplication(config, logs) {
60
62
  eventBus,
61
63
  operations,
62
64
  plugins,
63
- application: web.application,
64
65
  processes,
65
66
  cloud,
66
67
  computer,
@@ -1,22 +1,25 @@
1
- import type { Operation } from "#core/operations/types";
2
- import type { ProcessSnapshot } from "#plugins/process/types";
3
- import type { ComputerSettings } from "#plugins/computer/types";
4
1
  export type RuntimeEvent = {
5
2
  type: "operation";
6
- operation: Operation;
3
+ operationId: string;
4
+ pluginId: string;
7
5
  } | {
8
6
  type: "operations-cleared";
9
- deleted: number;
10
- preserved: number;
11
7
  } | {
12
8
  type: "process";
13
- process: ProcessSnapshot;
9
+ processId: string;
10
+ output?: boolean;
11
+ deleted?: boolean;
14
12
  } | {
15
13
  type: "computer-settings";
16
- settings: ComputerSettings;
17
14
  } | {
18
- type: "process-output";
19
- processId: string;
15
+ type: "computer-snapshot";
16
+ snapshotId: string;
17
+ } | {
18
+ type: "agent";
19
+ sessionId: string;
20
+ turnIds?: string[];
21
+ itemIds?: string[];
22
+ deleted?: boolean;
20
23
  };
21
24
  export declare class RuntimeEventBus {
22
25
  private readonly emitter;
@@ -10,6 +10,7 @@ export declare class ExternalAccessRegistry {
10
10
  constructor(config: ChatRoomConfig["auth"]);
11
11
  setCloud(state: ExternalAccessState): void;
12
12
  clearCloud(): void;
13
+ publicWebBaseUrl(): string | null;
13
14
  baseUrlForHost(kind: ExternalAccessKind, hostname: string): string | null;
14
15
  matches(kind: ExternalAccessKind, hostname: string): boolean;
15
16
  hasHost(hostname: string): boolean;
@@ -13,6 +13,9 @@ export class ExternalAccessRegistry {
13
13
  clearCloud() {
14
14
  this.cloud = { mcpBaseUrl: null, webBaseUrl: null };
15
15
  }
16
+ publicWebBaseUrl() {
17
+ return this.cloud.webBaseUrl ?? this.selfHosted.webBaseUrl;
18
+ }
16
19
  baseUrlForHost(kind, hostname) {
17
20
  for (const url of this.urls(kind))
18
21
  if (new URL(url).hostname === hostname)
@@ -0,0 +1,50 @@
1
+ export type GitChangeKind = "modified" | "added" | "deleted" | "renamed" | "copied" | "untracked" | "conflicted";
2
+ export interface GitChange {
3
+ path: string;
4
+ originalPath: string | null;
5
+ indexStatus: string;
6
+ workingTreeStatus: string;
7
+ kind: GitChangeKind;
8
+ }
9
+ export interface GitStatus {
10
+ branch: string | null;
11
+ head: string | null;
12
+ upstream: string | null;
13
+ ahead: number;
14
+ behind: number;
15
+ changes: GitChange[];
16
+ }
17
+ export interface GitDiff {
18
+ diff: string;
19
+ truncated: boolean;
20
+ }
21
+ export interface GitBranch {
22
+ name: string;
23
+ current: boolean;
24
+ upstream: string | null;
25
+ }
26
+ export interface GitCommit {
27
+ hash: string;
28
+ shortHash: string;
29
+ author: string;
30
+ date: string;
31
+ subject: string;
32
+ }
33
+ export interface GitAccess {
34
+ projectFiles(cwd: string, maxFiles?: number): Promise<string[] | null>;
35
+ status(cwd: string): Promise<GitStatus | null>;
36
+ diff(cwd: string, filePath?: string, maxPreviewBytes?: number): Promise<GitDiff>;
37
+ stage(cwd: string, paths: string[]): Promise<GitStatus>;
38
+ unstage(cwd: string, paths: string[]): Promise<GitStatus>;
39
+ restore(cwd: string, filePath: string): Promise<GitStatus>;
40
+ commit(cwd: string, message: string): Promise<GitStatus>;
41
+ branches(cwd: string): Promise<GitBranch[]>;
42
+ createBranch(cwd: string, name: string): Promise<GitStatus>;
43
+ switchBranch(cwd: string, name: string): Promise<GitStatus>;
44
+ deleteBranch(cwd: string, name: string): Promise<GitStatus>;
45
+ log(cwd: string, limit?: number): Promise<GitCommit[]>;
46
+ fetch(cwd: string): Promise<GitStatus>;
47
+ pull(cwd: string): Promise<GitStatus>;
48
+ push(cwd: string): Promise<GitStatus>;
49
+ }
50
+ export declare const GitAccessToken: import("#app/service-registry").ServiceToken<GitAccess>;
@@ -0,0 +1,2 @@
1
+ import { createServiceToken } from "#app/service-registry";
2
+ export const GitAccessToken = createServiceToken("git-access");
@@ -0,0 +1,11 @@
1
+ export interface ServiceToken<T> {
2
+ readonly key: symbol;
3
+ readonly name: string;
4
+ readonly __type?: T;
5
+ }
6
+ export declare function createServiceToken<T>(name: string): ServiceToken<T>;
7
+ export declare class ServiceRegistry {
8
+ private readonly values;
9
+ provide<T>(token: ServiceToken<T>, value: T): void;
10
+ require<T>(token: ServiceToken<T>): T;
11
+ }
@@ -0,0 +1,17 @@
1
+ export function createServiceToken(name) {
2
+ return { key: Symbol(name), name };
3
+ }
4
+ export class ServiceRegistry {
5
+ values = new Map();
6
+ provide(token, value) {
7
+ if (this.values.has(token.key))
8
+ throw new Error(`Service already provided: ${token.name}`);
9
+ this.values.set(token.key, value);
10
+ }
11
+ require(token) {
12
+ const value = this.values.get(token.key);
13
+ if (value === undefined)
14
+ throw new Error(`Required service unavailable: ${token.name}`);
15
+ return value;
16
+ }
17
+ }
@@ -0,0 +1,24 @@
1
+ export interface WorkspaceEntry {
2
+ root: string;
3
+ name: string;
4
+ summary: string | null;
5
+ }
6
+ export interface WorkspaceSkill {
7
+ name: string;
8
+ description: string;
9
+ path: string;
10
+ }
11
+ export interface WorkspaceInfo extends WorkspaceEntry {
12
+ presetPrompt: string | null;
13
+ instructions: string | null;
14
+ skills: WorkspaceSkill[];
15
+ }
16
+ export interface WorkspaceAccess {
17
+ roots(): string[];
18
+ list(): Promise<WorkspaceEntry[]>;
19
+ resolve(input: string): Promise<string>;
20
+ createProject(parentInput: string, nameInput: string): Promise<WorkspaceEntry>;
21
+ projectFiles(input: string, maxFiles?: number): Promise<string[]>;
22
+ info(input: string): Promise<WorkspaceInfo>;
23
+ }
24
+ export declare const WorkspaceAccessToken: import("#app/service-registry").ServiceToken<WorkspaceAccess>;
@@ -0,0 +1,2 @@
1
+ import { createServiceToken } from "#app/service-registry";
2
+ export const WorkspaceAccessToken = createServiceToken("workspace-access");
@@ -13,8 +13,10 @@ export declare class IngressPolicy {
13
13
  allowsHost(hostname: string): boolean;
14
14
  requiresWebAuth(req: Request): boolean;
15
15
  isExternalWeb(req: Request): boolean;
16
+ isRemoteWeb(req: Request): boolean;
16
17
  requiresMcpAuth(req: Request): boolean;
17
18
  isExternalMcp(req: Request): boolean;
19
+ isRemoteMcp(req: Request): boolean;
18
20
  secureWebCookie(req: Request): boolean;
19
21
  expectedWebOrigin(req: Request): string | null;
20
22
  webAuthnOrigin(req: Request): WebAuthnOrigin | null;
@@ -14,19 +14,25 @@ export class IngressPolicy {
14
14
  return (this.staticHosts.has(hostname) || this.externalAccess.hasHost(hostname));
15
15
  }
16
16
  requiresWebAuth(req) {
17
- if (this.isExternalWeb(req))
17
+ if (this.isRemoteWeb(req))
18
18
  return true;
19
19
  return this.config.auth.localWebAuth;
20
20
  }
21
21
  isExternalWeb(req) {
22
22
  return this.externalAccess.matches("web", req.hostname);
23
23
  }
24
+ isRemoteWeb(req) {
25
+ return this.externalAccess.hasHost(req.hostname) || !isLoopbackPeer(req);
26
+ }
24
27
  requiresMcpAuth(req) {
25
- return this.isExternalMcp(req);
28
+ return this.isRemoteMcp(req);
26
29
  }
27
30
  isExternalMcp(req) {
28
31
  return this.externalAccess.matches("mcp", req.hostname);
29
32
  }
33
+ isRemoteMcp(req) {
34
+ return this.externalAccess.hasHost(req.hostname) || !isLoopbackPeer(req);
35
+ }
30
36
  secureWebCookie(req) {
31
37
  const base = this.externalBaseUrl(req, "web");
32
38
  if (!base)
@@ -59,3 +65,9 @@ export class IngressPolicy {
59
65
  return this.externalAccess.baseUrlForHost(kind, req.hostname);
60
66
  }
61
67
  }
68
+ function isLoopbackPeer(req) {
69
+ const address = req.socket.remoteAddress;
70
+ return (address === "::1" ||
71
+ address?.startsWith("127.") === true ||
72
+ address?.startsWith("::ffff:127.") === true);
73
+ }
@@ -140,6 +140,6 @@ export class PasskeyService {
140
140
  }
141
141
  function requireOrigin(origin) {
142
142
  if (!origin)
143
- throw new ChatRoomError("UNSUPPORTED", "Passkeys require a secure public WebUI origin");
143
+ throw new ChatRoomError("UNSUPPORTED", "Passkeys require a secure public Web UI origin");
144
144
  return origin;
145
145
  }
@@ -13,7 +13,7 @@ async function init() {
13
13
  console.log(`ChatRoom initialized: ${result.configPath}`);
14
14
  console.log(`Allowed roots: ${result.config.allowedRoots.join(", ")}`);
15
15
  console.log(`Owner token: ${result.config.auth.ownerToken}`);
16
- console.log("Local loopback WebUI does not require authentication by default. Keep the owner token private for public access.");
16
+ console.log("Local loopback Web UI does not require authentication by default. Keep the owner token private for public access.");
17
17
  }
18
18
  async function serve() {
19
19
  const config = await loadConfig();
@@ -0,0 +1,9 @@
1
+ export declare function normalizeRelativePath(input: string): string;
2
+ export declare function resolveReadableWithin(root: string, relativePath: string): Promise<string>;
3
+ export declare function resolveReadableFileWithin(root: string, relativePath: string): Promise<{
4
+ absolute: string;
5
+ relative: string;
6
+ }>;
7
+ export declare function resolveWritableWithin(root: string, relativePath: string): Promise<string>;
8
+ export declare function writeBytesAtomicWithin(root: string, relativePath: string, content: Uint8Array, mode?: number): Promise<void>;
9
+ export declare function removeFileWithin(root: string, relativePath: string): Promise<void>;
@@ -0,0 +1,129 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ChatRoomError } from "#core/errors/chatroom-error";
5
+ export function normalizeRelativePath(input) {
6
+ if (typeof input !== "string" || input.includes("\0"))
7
+ throw new ChatRoomError("INVALID_INPUT", "Path must be a valid string");
8
+ if (path.isAbsolute(input) ||
9
+ path.win32.isAbsolute(input) ||
10
+ /^\\\\/.test(input))
11
+ throw new ChatRoomError("FORBIDDEN", "Absolute paths are not allowed");
12
+ const parts = input
13
+ .split(/[\\/]+/)
14
+ .filter((part) => part !== "" && part !== ".");
15
+ if (parts.some((part) => part === ".."))
16
+ throw new ChatRoomError("FORBIDDEN", "Path traversal is not allowed");
17
+ return parts.join("/") || ".";
18
+ }
19
+ export async function resolveReadableWithin(root, relativePath) {
20
+ const lexical = lexicalWithin(root, relativePath);
21
+ const canonical = await realpath(lexical).catch((error) => {
22
+ if (error.code === "ENOENT")
23
+ throw new ChatRoomError("NOT_FOUND", `Path does not exist: ${relativePath}`);
24
+ throw error;
25
+ });
26
+ if (!inside(root, canonical))
27
+ throw new ChatRoomError("FORBIDDEN", "Symlink escapes root directory");
28
+ return canonical;
29
+ }
30
+ export async function resolveReadableFileWithin(root, relativePath) {
31
+ const absolute = await resolveReadableWithin(root, relativePath);
32
+ if (!(await stat(absolute)).isFile())
33
+ throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
34
+ return {
35
+ absolute,
36
+ relative: path.relative(root, absolute).split(path.sep).join("/"),
37
+ };
38
+ }
39
+ export async function resolveWritableWithin(root, relativePath) {
40
+ const target = lexicalWithin(root, relativePath);
41
+ const relative = path.relative(root, target);
42
+ const parts = relative.split(path.sep).filter(Boolean);
43
+ let current = root;
44
+ for (let index = 0; index < parts.length - 1; index++) {
45
+ current = path.join(current, parts[index]);
46
+ let info;
47
+ try {
48
+ info = await lstat(current);
49
+ }
50
+ catch (error) {
51
+ if (error.code !== "ENOENT")
52
+ throw error;
53
+ try {
54
+ await mkdir(current, { recursive: false, mode: 0o700 });
55
+ }
56
+ catch (mkdirError) {
57
+ if (mkdirError.code !== "EEXIST")
58
+ throw mkdirError;
59
+ }
60
+ info = await lstat(current);
61
+ }
62
+ if (info.isSymbolicLink())
63
+ throw new ChatRoomError("FORBIDDEN", `Writes through symlinked directories are not allowed: ${relativePath}`);
64
+ if (!info.isDirectory())
65
+ throw new ChatRoomError("INVALID_INPUT", `Parent is not a directory: ${parts[index]}`);
66
+ const canonical = await realpath(current);
67
+ if (!inside(root, canonical))
68
+ throw new ChatRoomError("FORBIDDEN", "Write path escapes root directory");
69
+ }
70
+ try {
71
+ const targetInfo = await lstat(target);
72
+ if (targetInfo.isSymbolicLink())
73
+ throw new ChatRoomError("FORBIDDEN", `Writes through symlinks are not allowed: ${relativePath}`);
74
+ if (!targetInfo.isFile())
75
+ throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
76
+ }
77
+ catch (error) {
78
+ if (error instanceof ChatRoomError)
79
+ throw error;
80
+ if (error.code !== "ENOENT")
81
+ throw error;
82
+ }
83
+ return target;
84
+ }
85
+ export async function writeBytesAtomicWithin(root, relativePath, content, mode = 0o600) {
86
+ const target = await resolveWritableWithin(root, relativePath);
87
+ const temporary = `${target}.chatroom-${randomUUID()}.tmp`;
88
+ try {
89
+ await writeFile(temporary, content, { flag: "wx", mode });
90
+ await rename(temporary, target);
91
+ }
92
+ finally {
93
+ await rm(temporary, { force: true }).catch(() => undefined);
94
+ }
95
+ }
96
+ export async function removeFileWithin(root, relativePath) {
97
+ const target = lexicalWithin(root, relativePath);
98
+ const relative = path.relative(root, target);
99
+ const parts = relative.split(path.sep).filter(Boolean);
100
+ let current = root;
101
+ for (let index = 0; index < parts.length - 1; index++) {
102
+ current = path.join(current, parts[index]);
103
+ const info = await lstat(current);
104
+ if (info.isSymbolicLink())
105
+ throw new ChatRoomError("FORBIDDEN", `Removal through symlinked directories is not allowed: ${relativePath}`);
106
+ if (!info.isDirectory())
107
+ throw new ChatRoomError("INVALID_INPUT", `Parent is not a directory: ${parts[index]}`);
108
+ if (!inside(root, await realpath(current)))
109
+ throw new ChatRoomError("FORBIDDEN", "Removal path escapes root directory");
110
+ }
111
+ const info = await lstat(target);
112
+ if (!info.isFile() && !info.isSymbolicLink())
113
+ throw new ChatRoomError("INVALID_INPUT", `Not a file: ${relativePath}`);
114
+ await rm(target, { force: true });
115
+ }
116
+ function lexicalWithin(root, relativePath) {
117
+ const normalized = normalizeRelativePath(relativePath);
118
+ const absolute = path.resolve(root, ...normalized.split("/"));
119
+ if (!inside(root, absolute))
120
+ throw new ChatRoomError("FORBIDDEN", "Path escapes root directory");
121
+ return absolute;
122
+ }
123
+ function inside(root, candidate) {
124
+ const relative = path.relative(root, candidate);
125
+ return (relative === "" ||
126
+ (!relative.startsWith(`..${path.sep}`) &&
127
+ relative !== ".." &&
128
+ !path.isAbsolute(relative)));
129
+ }
@@ -2,6 +2,8 @@ import type { Operation, OperationStatus } from "./types.js";
2
2
  export interface OperationQuery {
3
3
  limit?: number;
4
4
  offset?: number;
5
+ beforeStartedAt?: string;
6
+ beforeOperationId?: string;
5
7
  pluginId?: string;
6
8
  status?: OperationStatus | string;
7
9
  }
@@ -0,0 +1,19 @@
1
+ export interface LineProcessOptions {
2
+ command: string;
3
+ args?: string[];
4
+ cwd?: string;
5
+ env?: Record<string, string>;
6
+ onStdout(line: string): void;
7
+ onStderr(line: string): void;
8
+ onExit(code: number | null, signal: NodeJS.Signals | null): void;
9
+ onError(error: Error): void;
10
+ }
11
+ export declare class LineProcess {
12
+ private readonly child;
13
+ constructor(options: LineProcessOptions);
14
+ spawned(): Promise<void>;
15
+ writeLine(line: string): void;
16
+ terminate(): void;
17
+ forceKill(): void;
18
+ waitForExit(timeoutMs: number): Promise<void>;
19
+ }
@@ -0,0 +1,51 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import { childEnvironment } from "./child-environment.js";
4
+ export class LineProcess {
5
+ child;
6
+ constructor(options) {
7
+ this.child = spawn(options.command, options.args ?? [], {
8
+ ...(options.cwd ? { cwd: options.cwd } : {}),
9
+ env: childEnvironment(options.env),
10
+ stdio: ["pipe", "pipe", "pipe"],
11
+ windowsHide: true,
12
+ });
13
+ createInterface({ input: this.child.stdout }).on("line", options.onStdout);
14
+ createInterface({ input: this.child.stderr }).on("line", options.onStderr);
15
+ this.child.once("exit", options.onExit);
16
+ this.child.once("error", options.onError);
17
+ }
18
+ async spawned() {
19
+ if (this.child.pid)
20
+ return;
21
+ await new Promise((resolve, reject) => {
22
+ this.child.once("spawn", resolve);
23
+ this.child.once("error", reject);
24
+ });
25
+ }
26
+ writeLine(line) {
27
+ if (!this.child.stdin.writable)
28
+ throw new Error("Child process stdin is not writable");
29
+ this.child.stdin.write(line + "\n");
30
+ }
31
+ terminate() {
32
+ if (this.child.exitCode === null)
33
+ this.child.kill("SIGTERM");
34
+ }
35
+ forceKill() {
36
+ if (this.child.exitCode === null)
37
+ this.child.kill("SIGKILL");
38
+ }
39
+ async waitForExit(timeoutMs) {
40
+ if (this.child.exitCode !== null)
41
+ return;
42
+ await new Promise((resolve) => {
43
+ const timer = setTimeout(resolve, timeoutMs);
44
+ timer.unref();
45
+ this.child.once("exit", () => {
46
+ clearTimeout(timer);
47
+ resolve();
48
+ });
49
+ });
50
+ }
51
+ }
@@ -42,11 +42,17 @@ export class OperationRepository {
42
42
  where.push("status=?");
43
43
  args.push(query.status);
44
44
  }
45
+ if (query.beforeStartedAt && query.beforeOperationId) {
46
+ where.push("(started_at < ? OR (started_at = ? AND operation_id < ?))");
47
+ args.push(query.beforeStartedAt, query.beforeStartedAt, query.beforeOperationId);
48
+ }
45
49
  const limit = Math.min(Math.max(query.limit ?? 100, 1), 500);
46
- const offset = Math.max(query.offset ?? 0, 0);
50
+ const offset = query.beforeStartedAt && query.beforeOperationId
51
+ ? 0
52
+ : Math.max(query.offset ?? 0, 0);
47
53
  const filter = where.length ? `WHERE ${where.join(" AND ")}` : "";
48
54
  return this.database.raw
49
- .prepare(`SELECT * FROM operations ${filter} ORDER BY started_at DESC LIMIT ? OFFSET ?`)
55
+ .prepare(`SELECT * FROM operations ${filter} ORDER BY started_at DESC, operation_id DESC LIMIT ? OFFSET ?`)
50
56
  .all(...args, limit, offset).map(fromRow);
51
57
  }
52
58
  reconcileRunning(finishedAt) {
@@ -1 +1 @@
1
- export declare const DATABASE_SCHEMA = "\n CREATE TABLE IF NOT EXISTS operations (\n operation_id TEXT PRIMARY KEY,\n plugin_id TEXT NOT NULL,\n source TEXT NOT NULL,\n action TEXT NOT NULL,\n status TEXT NOT NULL,\n process_id TEXT,\n input_json TEXT NOT NULL,\n output_json TEXT NOT NULL,\n error_json TEXT NOT NULL,\n input_truncated INTEGER NOT NULL DEFAULT 0,\n output_truncated INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n finished_at TEXT,\n duration_ms INTEGER\n );\n CREATE INDEX IF NOT EXISTS operations_started_idx ON operations(started_at DESC);\n CREATE INDEX IF NOT EXISTS operations_plugin_idx ON operations(plugin_id, started_at DESC);\n CREATE INDEX IF NOT EXISTS operations_status_idx ON operations(status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS oauth_clients (\n client_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n redirect_uris_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS oauth_codes (\n code_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n redirect_uri TEXT NOT NULL,\n code_challenge TEXT NOT NULL,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n used_at TEXT\n );\n CREATE INDEX IF NOT EXISTS oauth_code_expiry_idx ON oauth_codes(expires_at);\n CREATE TABLE IF NOT EXISTS oauth_tokens (\n token_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS oauth_token_expiry_idx ON oauth_tokens(expires_at);\n CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (\n token_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS oauth_refresh_expiry_idx ON oauth_refresh_tokens(expires_at);\n\n CREATE TABLE IF NOT EXISTS web_sessions (\n token_hash TEXT PRIMARY KEY,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS web_session_expiry_idx ON web_sessions(expires_at);\n\n CREATE TABLE IF NOT EXISTS passkeys (\n credential_id TEXT PRIMARY KEY,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL,\n transports_json TEXT NOT NULL,\n device_type TEXT NOT NULL CHECK(device_type IN ('singleDevice','multiDevice')),\n backed_up INTEGER NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS passkeys_last_used_idx ON passkeys(last_used_at DESC);\n\n CREATE TABLE IF NOT EXISTS computer_settings (\n id INTEGER PRIMARY KEY CHECK(id = 1),\n enabled INTEGER NOT NULL DEFAULT 0,\n remote_access INTEGER NOT NULL DEFAULT 1,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS mcp_tool_settings (\n tool_name TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),\n updated_at TEXT NOT NULL\n );\n\n";
1
+ export declare const DATABASE_SCHEMA = "\n CREATE TABLE IF NOT EXISTS operations (\n operation_id TEXT PRIMARY KEY,\n plugin_id TEXT NOT NULL,\n source TEXT NOT NULL,\n action TEXT NOT NULL,\n status TEXT NOT NULL,\n process_id TEXT,\n input_json TEXT NOT NULL,\n output_json TEXT NOT NULL,\n error_json TEXT NOT NULL,\n input_truncated INTEGER NOT NULL DEFAULT 0,\n output_truncated INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n finished_at TEXT,\n duration_ms INTEGER\n );\n CREATE INDEX IF NOT EXISTS operations_started_idx ON operations(started_at DESC);\n CREATE INDEX IF NOT EXISTS operations_plugin_idx ON operations(plugin_id, started_at DESC);\n CREATE INDEX IF NOT EXISTS operations_status_idx ON operations(status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS oauth_clients (\n client_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n redirect_uris_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS oauth_codes (\n code_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n redirect_uri TEXT NOT NULL,\n code_challenge TEXT NOT NULL,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n used_at TEXT\n );\n CREATE INDEX IF NOT EXISTS oauth_code_expiry_idx ON oauth_codes(expires_at);\n CREATE TABLE IF NOT EXISTS oauth_tokens (\n token_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS oauth_token_expiry_idx ON oauth_tokens(expires_at);\n CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (\n token_hash TEXT PRIMARY KEY,\n client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,\n scope TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n revoked_at TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS oauth_refresh_expiry_idx ON oauth_refresh_tokens(expires_at);\n\n CREATE TABLE IF NOT EXISTS web_sessions (\n token_hash TEXT PRIMARY KEY,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS web_session_expiry_idx ON web_sessions(expires_at);\n\n CREATE TABLE IF NOT EXISTS passkeys (\n credential_id TEXT PRIMARY KEY,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL,\n transports_json TEXT NOT NULL,\n device_type TEXT NOT NULL CHECK(device_type IN ('singleDevice','multiDevice')),\n backed_up INTEGER NOT NULL,\n name TEXT NOT NULL,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS passkeys_last_used_idx ON passkeys(last_used_at DESC);\n\n CREATE TABLE IF NOT EXISTS computer_settings (\n id INTEGER PRIMARY KEY CHECK(id = 1),\n enabled INTEGER NOT NULL DEFAULT 0,\n remote_access INTEGER NOT NULL DEFAULT 1,\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS mcp_tool_settings (\n tool_name TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL CHECK(enabled IN (0, 1)),\n updated_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_sessions (\n id TEXT PRIMARY KEY,\n workspace_root TEXT NOT NULL,\n provider TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('idle','running','waiting_input','error')),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS agent_sessions_updated_idx ON agent_sessions(updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_turns (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,\n provider_turn_id TEXT,\n provider TEXT NOT NULL,\n model TEXT,\n reasoning_effort TEXT,\n reasoning_summary TEXT CHECK(reasoning_summary IN ('auto','concise','detailed','none')),\n service_tier TEXT,\n approval_policy_json TEXT,\n approvals_reviewer TEXT CHECK(approvals_reviewer IN ('user','auto_review','guardian_subagent')),\n permission_mode TEXT CHECK(permission_mode IN ('default','acceptEdits','bypassPermissions','plan','dontAsk','auto','read-only','workspace-write','danger-full-access')),\n usage_json TEXT,\n origin INTEGER NOT NULL CHECK(origin IN (0,1)),\n status TEXT NOT NULL CHECK(status IN ('running','completed','interrupted','failed')),\n error_json TEXT NOT NULL,\n started_at TEXT NOT NULL,\n completed_at TEXT\n );\n CREATE INDEX IF NOT EXISTS agent_turns_session_idx ON agent_turns(session_id, started_at ASC);\n\n CREATE TABLE IF NOT EXISTS agent_items (\n id TEXT PRIMARY KEY,\n turn_id TEXT NOT NULL REFERENCES agent_turns(id) ON DELETE CASCADE,\n provider_item_id TEXT,\n type TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('running','completed','waiting','interrupted','failed')),\n payload_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS agent_items_turn_idx ON agent_items(turn_id, sequence ASC);\n\n CREATE TABLE IF NOT EXISTS agent_session_providers (\n session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n provider_session_id TEXT NOT NULL,\n model TEXT,\n reasoning_effort TEXT,\n reasoning_summary TEXT CHECK(reasoning_summary IN ('auto','concise','detailed','none')),\n service_tier TEXT,\n approval_policy_json TEXT,\n approvals_reviewer TEXT CHECK(approvals_reviewer IN ('user','auto_review','guardian_subagent')),\n permission_mode TEXT CHECK(permission_mode IN ('default','acceptEdits','bypassPermissions','plan','dontAsk','auto','read-only','workspace-write','danger-full-access')),\n token_usage_json TEXT,\n continuity_context TEXT NOT NULL DEFAULT '',\n synced_through_turn_id TEXT REFERENCES agent_turns(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY(session_id, provider)\n );\n";
@@ -87,4 +87,66 @@ export const DATABASE_SCHEMA = `
87
87
  updated_at TEXT NOT NULL
88
88
  );
89
89
 
90
+ CREATE TABLE IF NOT EXISTS agent_sessions (
91
+ id TEXT PRIMARY KEY,
92
+ workspace_root TEXT NOT NULL,
93
+ provider TEXT NOT NULL,
94
+ status TEXT NOT NULL CHECK(status IN ('idle','running','waiting_input','error')),
95
+ created_at TEXT NOT NULL,
96
+ updated_at TEXT NOT NULL
97
+ );
98
+ CREATE INDEX IF NOT EXISTS agent_sessions_updated_idx ON agent_sessions(updated_at DESC);
99
+
100
+ CREATE TABLE IF NOT EXISTS agent_turns (
101
+ id TEXT PRIMARY KEY,
102
+ session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
103
+ provider_turn_id TEXT,
104
+ provider TEXT NOT NULL,
105
+ model TEXT,
106
+ reasoning_effort TEXT,
107
+ reasoning_summary TEXT CHECK(reasoning_summary IN ('auto','concise','detailed','none')),
108
+ service_tier TEXT,
109
+ approval_policy_json TEXT,
110
+ approvals_reviewer TEXT CHECK(approvals_reviewer IN ('user','auto_review','guardian_subagent')),
111
+ permission_mode TEXT CHECK(permission_mode IN ('default','acceptEdits','bypassPermissions','plan','dontAsk','auto','read-only','workspace-write','danger-full-access')),
112
+ usage_json TEXT,
113
+ origin INTEGER NOT NULL CHECK(origin IN (0,1)),
114
+ status TEXT NOT NULL CHECK(status IN ('running','completed','interrupted','failed')),
115
+ error_json TEXT NOT NULL,
116
+ started_at TEXT NOT NULL,
117
+ completed_at TEXT
118
+ );
119
+ CREATE INDEX IF NOT EXISTS agent_turns_session_idx ON agent_turns(session_id, started_at ASC);
120
+
121
+ CREATE TABLE IF NOT EXISTS agent_items (
122
+ id TEXT PRIMARY KEY,
123
+ turn_id TEXT NOT NULL REFERENCES agent_turns(id) ON DELETE CASCADE,
124
+ provider_item_id TEXT,
125
+ type TEXT NOT NULL,
126
+ sequence INTEGER NOT NULL,
127
+ status TEXT NOT NULL CHECK(status IN ('running','completed','waiting','interrupted','failed')),
128
+ payload_json TEXT NOT NULL,
129
+ created_at TEXT NOT NULL,
130
+ updated_at TEXT NOT NULL
131
+ );
132
+ CREATE INDEX IF NOT EXISTS agent_items_turn_idx ON agent_items(turn_id, sequence ASC);
133
+
134
+ CREATE TABLE IF NOT EXISTS agent_session_providers (
135
+ session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
136
+ provider TEXT NOT NULL,
137
+ provider_session_id TEXT NOT NULL,
138
+ model TEXT,
139
+ reasoning_effort TEXT,
140
+ reasoning_summary TEXT CHECK(reasoning_summary IN ('auto','concise','detailed','none')),
141
+ service_tier TEXT,
142
+ approval_policy_json TEXT,
143
+ approvals_reviewer TEXT CHECK(approvals_reviewer IN ('user','auto_review','guardian_subagent')),
144
+ permission_mode TEXT CHECK(permission_mode IN ('default','acceptEdits','bypassPermissions','plan','dontAsk','auto','read-only','workspace-write','danger-full-access')),
145
+ token_usage_json TEXT,
146
+ continuity_context TEXT NOT NULL DEFAULT '',
147
+ synced_through_turn_id TEXT REFERENCES agent_turns(id) ON DELETE SET NULL,
148
+ created_at TEXT NOT NULL,
149
+ updated_at TEXT NOT NULL,
150
+ PRIMARY KEY(session_id, provider)
151
+ );
90
152
  `;
@@ -65,7 +65,7 @@ export class HttpServer {
65
65
  this.mcpRequestCount += 1;
66
66
  next();
67
67
  }, mcpAuthentication(this.auth, this.ingress), (req, res) => {
68
- const scope = this.ingress.isExternalMcp(req) ? "remote" : "local";
68
+ const scope = this.ingress.isRemoteMcp(req) ? "remote" : "local";
69
69
  runWithMcpAccessScope(scope, () => {
70
70
  void nodeMcp(req, res, req.body);
71
71
  });