@nebutra/agent-runtime 0.2.0 → 0.2.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.
Files changed (137) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +2 -0
  3. package/dist/adapters/dispatcher-sse.js +1 -0
  4. package/dist/adapters/index.d.ts +24 -5
  5. package/dist/adapters/index.js +28 -0
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/adapters/mcp-catalog.js +1 -0
  8. package/dist/adapters/prisma-rollout.js +1 -0
  9. package/dist/chunk-424PT5DM.js +23 -0
  10. package/dist/chunk-424PT5DM.js.map +1 -0
  11. package/dist/{chunk-NN7DATXA.js → chunk-4Y25ZTKI.js} +3 -3
  12. package/dist/chunk-4Y25ZTKI.js.map +1 -0
  13. package/dist/{chunk-BJBBR3QA.js → chunk-D4YAPLOW.js} +4 -4
  14. package/dist/chunk-D4YAPLOW.js.map +1 -0
  15. package/dist/{chunk-ZMYX5VBU.js → chunk-GQZKYWFT.js} +23 -7
  16. package/dist/chunk-GQZKYWFT.js.map +1 -0
  17. package/dist/chunk-KCNN4QUQ.js +255 -0
  18. package/dist/chunk-KCNN4QUQ.js.map +1 -0
  19. package/dist/{chunk-PGGWSUTM.js → chunk-NI4EDT4T.js} +2 -2
  20. package/dist/chunk-NI4EDT4T.js.map +1 -0
  21. package/dist/chunk-Q62VKHIT.js +178 -0
  22. package/dist/chunk-Q62VKHIT.js.map +1 -0
  23. package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
  24. package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
  25. package/dist/{chunk-5N4644PB.js → chunk-SD2ZJ7XG.js} +4 -4
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +52 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/commands.js +1 -0
  30. package/dist/definitions.js +1 -0
  31. package/dist/dispatcher.js +1 -0
  32. package/dist/durable-turn.js +3 -2
  33. package/dist/hook-pipeline.js +1 -0
  34. package/dist/index.d.ts +8 -85
  35. package/dist/index.js +249 -134
  36. package/dist/index.js.map +1 -1
  37. package/dist/loop.d.ts +2 -2
  38. package/dist/loop.js +3 -2
  39. package/dist/mcp-bridge.d.ts +3 -3
  40. package/dist/mcp-bridge.js +3 -2
  41. package/dist/model.js +1 -0
  42. package/dist/orchestration.d.ts +84 -0
  43. package/dist/orchestration.js +16 -0
  44. package/dist/orchestration.js.map +1 -0
  45. package/dist/policy.js +1 -0
  46. package/dist/protocol.js +1 -0
  47. package/dist/pulsar.d.ts +78 -0
  48. package/dist/pulsar.js +17 -0
  49. package/dist/pulsar.js.map +1 -0
  50. package/dist/rollout-store-persistent.js +1 -0
  51. package/dist/rollout.js +1 -0
  52. package/dist/sandbox.js +2 -1
  53. package/dist/skills.js +2 -1
  54. package/dist/subagents.js +1 -0
  55. package/dist/tools.d.ts +4 -3
  56. package/dist/tools.js +5 -3
  57. package/package.json +84 -27
  58. package/.turbo/turbo-build.log +0 -115
  59. package/.turbo/turbo-test.log +0 -44
  60. package/.turbo/turbo-typecheck.log +0 -4
  61. package/CHANGELOG.md +0 -253
  62. package/dist/chunk-BJBBR3QA.js.map +0 -1
  63. package/dist/chunk-NN7DATXA.js.map +0 -1
  64. package/dist/chunk-PGGWSUTM.js.map +0 -1
  65. package/dist/chunk-ZMYX5VBU.js.map +0 -1
  66. package/src/adapters/dispatcher-sse.test.ts +0 -218
  67. package/src/adapters/dispatcher-sse.ts +0 -222
  68. package/src/adapters/index.ts +0 -18
  69. package/src/adapters/mcp-catalog.test.ts +0 -213
  70. package/src/adapters/mcp-catalog.ts +0 -188
  71. package/src/adapters/prisma-rollout.test.ts +0 -153
  72. package/src/adapters/prisma-rollout.ts +0 -104
  73. package/src/agent-runtime.test.ts +0 -176
  74. package/src/artifact-stream.test.ts +0 -330
  75. package/src/artifact-stream.ts +0 -453
  76. package/src/channel-gateway.test.ts +0 -432
  77. package/src/channel-gateway.ts +0 -357
  78. package/src/code-review.test.ts +0 -501
  79. package/src/code-review.ts +0 -495
  80. package/src/command-suggestions.test.ts +0 -251
  81. package/src/command-suggestions.ts +0 -338
  82. package/src/commands.test.ts +0 -184
  83. package/src/commands.ts +0 -140
  84. package/src/commit-message.test.ts +0 -249
  85. package/src/commit-message.ts +0 -180
  86. package/src/context-compaction.test.ts +0 -522
  87. package/src/context-compaction.ts +0 -434
  88. package/src/definitions.test.ts +0 -78
  89. package/src/definitions.ts +0 -190
  90. package/src/deployment-status.test.ts +0 -215
  91. package/src/deployment-status.ts +0 -227
  92. package/src/design-context.test.ts +0 -195
  93. package/src/design-context.ts +0 -198
  94. package/src/dispatcher.test.ts +0 -234
  95. package/src/dispatcher.ts +0 -189
  96. package/src/durable-turn.test.ts +0 -209
  97. package/src/durable-turn.ts +0 -135
  98. package/src/edit-planner.test.ts +0 -204
  99. package/src/edit-planner.ts +0 -325
  100. package/src/fuzzy-match.test.ts +0 -311
  101. package/src/fuzzy-match.ts +0 -444
  102. package/src/hook-pipeline.test.ts +0 -279
  103. package/src/hook-pipeline.ts +0 -373
  104. package/src/inbound-admission.test.ts +0 -394
  105. package/src/inbound-admission.ts +0 -246
  106. package/src/index.ts +0 -47
  107. package/src/loop.test.ts +0 -161
  108. package/src/loop.ts +0 -211
  109. package/src/mcp-bridge.test.ts +0 -165
  110. package/src/mcp-bridge.ts +0 -76
  111. package/src/memory-provider.test.ts +0 -232
  112. package/src/memory-provider.ts +0 -257
  113. package/src/model.ts +0 -168
  114. package/src/permission-ruleset.test.ts +0 -301
  115. package/src/permission-ruleset.ts +0 -200
  116. package/src/policy.ts +0 -151
  117. package/src/project-repo.test.ts +0 -232
  118. package/src/project-repo.ts +0 -311
  119. package/src/protocol.ts +0 -159
  120. package/src/rollout-store-persistent.test.ts +0 -217
  121. package/src/rollout-store-persistent.ts +0 -166
  122. package/src/rollout.ts +0 -150
  123. package/src/sandbox.ts +0 -113
  124. package/src/session-share.test.ts +0 -360
  125. package/src/session-share.ts +0 -310
  126. package/src/skill-distillation.test.ts +0 -177
  127. package/src/skill-distillation.ts +0 -369
  128. package/src/skills.test.ts +0 -277
  129. package/src/skills.ts +0 -255
  130. package/src/subagents.test.ts +0 -290
  131. package/src/subagents.ts +0 -332
  132. package/src/tools.ts +0 -126
  133. package/src/workbench.test.ts +0 -0
  134. package/src/workbench.ts +0 -0
  135. package/tsconfig.json +0 -12
  136. package/tsup.config.ts +0 -33
  137. /package/dist/{chunk-5N4644PB.js.map → chunk-SD2ZJ7XG.js.map} +0 -0
@@ -1,232 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "vitest";
2
- import {
3
- appendJsonlLine,
4
- CONVERSATIONS_DIR,
5
- type CommitRef,
6
- emptyProjectMetadata,
7
- type GitHostPort,
8
- METADATA_PATH,
9
- type OwnsRepoPredicate,
10
- ProjectRepo,
11
- ProjectRepoError,
12
- parseJsonl,
13
- } from "./project-repo.js";
14
-
15
- /**
16
- * Fake git host: content-addressed by branch HEAD plus per-sha snapshots.
17
- * Faithfully models "every project is a git repo": writeCommit appends a
18
- * commit to the branch and freezes a full file snapshot under its sha.
19
- */
20
- class FakeGitHost implements GitHostPort {
21
- defaultBranch = "main";
22
- // repoId -> branch -> ordered commit shas
23
- private branches = new Map<string, Map<string, string[]>>();
24
- // repoId -> sha -> full file snapshot
25
- private snapshots = new Map<string, Map<string, Record<string, string>>>();
26
- private commitMeta = new Map<string, CommitRef>();
27
- public writeCount = 0;
28
- private seq = 0;
29
-
30
- async getDefaultBranch(): Promise<string> {
31
- return this.defaultBranch;
32
- }
33
-
34
- private head(repoId: string, branch: string): string | null {
35
- const shas = this.branches.get(repoId)?.get(branch);
36
- if (!shas || shas.length === 0) return null;
37
- return shas[shas.length - 1] ?? null;
38
- }
39
-
40
- async readFile(repoId: string, path: string, ref?: string | undefined): Promise<string | null> {
41
- const sha = ref ?? this.head(repoId, this.defaultBranch);
42
- if (!sha) return null;
43
- const snap = this.snapshots.get(repoId)?.get(sha);
44
- if (!snap) return null;
45
- return snap[path] ?? null;
46
- }
47
-
48
- async writeCommit(
49
- repoId: string,
50
- branch: string,
51
- files: Record<string, string>,
52
- message: string,
53
- ): Promise<CommitRef> {
54
- this.writeCount += 1;
55
- const prevSha = this.head(repoId, branch);
56
- const prevSnap = (prevSha ? this.snapshots.get(repoId)?.get(prevSha) : undefined) ?? {};
57
- const nextSnap = { ...prevSnap, ...files };
58
- this.seq += 1;
59
- const sha = `sha-${this.seq}`;
60
- if (!this.snapshots.has(repoId)) this.snapshots.set(repoId, new Map());
61
- this.snapshots.get(repoId)!.set(sha, nextSnap);
62
- if (!this.branches.has(repoId)) this.branches.set(repoId, new Map());
63
- const bmap = this.branches.get(repoId)!;
64
- if (!bmap.has(branch)) bmap.set(branch, []);
65
- bmap.get(branch)!.push(sha);
66
- const ref: CommitRef = {
67
- sha,
68
- message,
69
- date: new Date(2026, 0, this.seq).toISOString(),
70
- };
71
- this.commitMeta.set(sha, ref);
72
- return ref;
73
- }
74
-
75
- async listCommits(
76
- repoId: string,
77
- branch: string,
78
- limit?: number | undefined,
79
- ): Promise<CommitRef[]> {
80
- const shas = this.branches.get(repoId)?.get(branch) ?? [];
81
- const refs = [...shas]
82
- .reverse()
83
- .map((s) => this.commitMeta.get(s))
84
- .filter((r): r is CommitRef => Boolean(r));
85
- return typeof limit === "number" ? refs.slice(0, limit) : refs;
86
- }
87
- }
88
-
89
- const T = "tenant-1";
90
- const REPO = "tenant-1/proj-a";
91
- const ownsAll: OwnsRepoPredicate = () => true;
92
-
93
- function makeRepo(host: FakeGitHost, owns: OwnsRepoPredicate = ownsAll): ProjectRepo {
94
- return new ProjectRepo({ host, ownsRepo: owns });
95
- }
96
-
97
- describe("pure helpers", () => {
98
- it("parseJsonl tolerates blank and trailing newline", () => {
99
- expect(parseJsonl("")).toEqual([]);
100
- expect(parseJsonl("\n\n")).toEqual([]);
101
- expect(parseJsonl('{"a":1}\n{"b":2}\n')).toEqual([{ a: 1 }, { b: 2 }]);
102
- expect(parseJsonl('{"a":1}')).toEqual([{ a: 1 }]);
103
- });
104
-
105
- it("appendJsonlLine is immutable and appends a single line", () => {
106
- const prev = '{"a":1}\n';
107
- const next = appendJsonlLine(prev, { b: 2 });
108
- expect(next).toBe('{"a":1}\n{"b":2}\n');
109
- expect(prev).toBe('{"a":1}\n');
110
- expect(appendJsonlLine("", { x: 1 })).toBe('{"x":1}\n');
111
- expect(appendJsonlLine('{"a":1}', { b: 2 })).toBe('{"a":1}\n{"b":2}\n');
112
- });
113
-
114
- it("emptyProjectMetadata is well-formed", () => {
115
- const m = emptyProjectMetadata("proj-a", "Proj A");
116
- expect(m.projectId).toBe("proj-a");
117
- expect(m.conversations).toEqual([]);
118
- expect(m.deployments).toEqual([]);
119
- });
120
- });
121
-
122
- describe("ProjectRepo metadata", () => {
123
- let host: FakeGitHost;
124
- let repo: ProjectRepo;
125
- beforeEach(() => {
126
- host = new FakeGitHost();
127
- repo = makeRepo(host);
128
- });
129
-
130
- it("reads missing metadata as empty without throwing", async () => {
131
- const m = await repo.readMetadata(T, REPO);
132
- expect(m.conversations).toEqual([]);
133
- expect(m.deployments).toEqual([]);
134
- });
135
-
136
- it("throws typed error on malformed JSON metadata", async () => {
137
- await host.writeCommit(REPO, "main", { [METADATA_PATH]: "{not json" }, "x");
138
- await expect(repo.readMetadata(T, REPO)).rejects.toBeInstanceOf(ProjectRepoError);
139
- });
140
-
141
- it("write -> read round-trips", async () => {
142
- const meta = emptyProjectMetadata("proj-a", "Proj A");
143
- const input = { ...meta, productionDomain: "proj-a.example.com" };
144
- const ref = await repo.writeMetadata(T, REPO, input);
145
- expect(ref.sha).toBeTruthy();
146
- const back = await repo.readMetadata(T, REPO);
147
- expect(back.productionDomain).toBe("proj-a.example.com");
148
- // input not mutated
149
- expect(input.conversations).toEqual([]);
150
- });
151
- });
152
-
153
- describe("ProjectRepo conversations", () => {
154
- let host: FakeGitHost;
155
- let repo: ProjectRepo;
156
- beforeEach(() => {
157
- host = new FakeGitHost();
158
- repo = makeRepo(host);
159
- });
160
-
161
- it("appends one commit per call and accumulates", async () => {
162
- await repo.appendConversationMessage(T, REPO, "c1", { role: "user", text: "hi" });
163
- expect(host.writeCount).toBe(1);
164
- await repo.appendConversationMessage(T, REPO, "c1", { role: "assistant", text: "yo" });
165
- expect(host.writeCount).toBe(2);
166
- const msgs = await repo.readConversationMessages(T, REPO, "c1");
167
- expect(msgs).toEqual([
168
- { role: "user", text: "hi" },
169
- { role: "assistant", text: "yo" },
170
- ]);
171
- });
172
-
173
- it("readConversationMessages tolerates blank / missing file", async () => {
174
- expect(await repo.readConversationMessages(T, REPO, "none")).toEqual([]);
175
- await host.writeCommit(REPO, "main", { [`${CONVERSATIONS_DIR}/c2.jsonl`]: "\n\n" }, "blank");
176
- expect(await repo.readConversationMessages(T, REPO, "c2")).toEqual([]);
177
- });
178
- });
179
-
180
- describe("ProjectRepo history & restore", () => {
181
- let host: FakeGitHost;
182
- let repo: ProjectRepo;
183
- beforeEach(() => {
184
- host = new FakeGitHost();
185
- repo = makeRepo(host);
186
- });
187
-
188
- it("restoreAt reads metadata at a specific sha", async () => {
189
- const r1 = await repo.writeMetadata(T, REPO, {
190
- ...emptyProjectMetadata("proj-a", "v1"),
191
- });
192
- await repo.writeMetadata(T, REPO, {
193
- ...emptyProjectMetadata("proj-a", "v2"),
194
- });
195
- const restored = await repo.restoreAt(T, REPO, r1.sha);
196
- expect(restored.metadata.name).toBe("v1");
197
- const head = await repo.readMetadata(T, REPO);
198
- expect(head.name).toBe("v2");
199
- });
200
-
201
- it("historyFromCommits delegates to the port", async () => {
202
- await repo.writeMetadata(T, REPO, emptyProjectMetadata("proj-a", "v1"));
203
- await repo.writeMetadata(T, REPO, emptyProjectMetadata("proj-a", "v2"));
204
- const hist = await repo.historyFromCommits(T, REPO, 1);
205
- expect(hist).toHaveLength(1);
206
- });
207
- });
208
-
209
- describe("multi-tenant fail-closed", () => {
210
- let host: FakeGitHost;
211
- beforeEach(() => {
212
- host = new FakeGitHost();
213
- });
214
-
215
- it("empty tenant throws", async () => {
216
- const repo = makeRepo(host);
217
- await expect(repo.readMetadata("", REPO)).rejects.toBeInstanceOf(ProjectRepoError);
218
- await expect(repo.readMetadata(" ", REPO)).rejects.toBeInstanceOf(ProjectRepoError);
219
- });
220
-
221
- it("denies cross-tenant repo access (ownsRepo default-deny)", async () => {
222
- const repo = makeRepo(host, (tenant, repoId) => repoId.startsWith(`${tenant}/`));
223
- await expect(repo.readMetadata("tenant-2", "tenant-1/proj-a")).rejects.toBeInstanceOf(
224
- ProjectRepoError,
225
- );
226
- });
227
-
228
- it("default ownsRepo is deny when not provided", async () => {
229
- const repo = new ProjectRepo({ host });
230
- await expect(repo.readMetadata(T, REPO)).rejects.toBeInstanceOf(ProjectRepoError);
231
- });
232
- });
@@ -1,311 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /**
4
- * Git-backed project repository model.
5
- *
6
- * Faithful re-expression of the "every project is a git repo" persistence
7
- * model: a project's metadata and conversation logs live AS committed files
8
- * inside its repository. The commit graph IS the history — there is no
9
- * in-memory snapshot ring (that is `workbench.ts`'s concern). The actual
10
- * provider-specific git implementation is injected via `GitHostPort`; this
11
- * module never performs git operations itself.
12
- */
13
-
14
- // ---------------------------------------------------------------------------
15
- // Domain types
16
- // ---------------------------------------------------------------------------
17
-
18
- export interface ConversationSummary {
19
- id: string;
20
- title: string;
21
- createdAt: string;
22
- updatedAt: string;
23
- }
24
-
25
- export type DeploymentState = "idle" | "deploying" | "live" | "failed";
26
-
27
- export interface DeploymentSummary {
28
- commitSha: string;
29
- commitMessage: string;
30
- commitDate: string;
31
- domain: string;
32
- url: string;
33
- deploymentId: string | null;
34
- state: DeploymentState;
35
- }
36
-
37
- export interface ProjectMetadata {
38
- projectId: string;
39
- name: string;
40
- conversations: ConversationSummary[];
41
- deployments: DeploymentSummary[];
42
- productionDomain?: string | undefined;
43
- productionDeploymentId?: string | undefined;
44
- }
45
-
46
- export interface CommitRef {
47
- sha: string;
48
- message: string;
49
- date: string;
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // Injected git-host port (provider-specific git impl is NOT ported here)
54
- // ---------------------------------------------------------------------------
55
-
56
- export interface GitHostPort {
57
- getDefaultBranch(repoId: string): Promise<string>;
58
- readFile(repoId: string, path: string, ref?: string | undefined): Promise<string | null>;
59
- writeCommit(
60
- repoId: string,
61
- branch: string,
62
- files: Record<string, string>,
63
- message: string,
64
- ): Promise<CommitRef>;
65
- listCommits(repoId: string, branch: string, limit?: number | undefined): Promise<CommitRef[]>;
66
- }
67
-
68
- /** Default-deny ownership predicate: cross-tenant access impossible unless wired. */
69
- export type OwnsRepoPredicate = (tenantId: string, repoId: string) => boolean | Promise<boolean>;
70
-
71
- export interface ProjectRepoDeps {
72
- host: GitHostPort;
73
- ownsRepo?: OwnsRepoPredicate | undefined;
74
- }
75
-
76
- // ---------------------------------------------------------------------------
77
- // Typed error
78
- // ---------------------------------------------------------------------------
79
-
80
- export type ProjectRepoErrorCode = "tenant_required" | "tenant_denied" | "metadata_malformed";
81
-
82
- export class ProjectRepoError extends Error {
83
- public readonly code: ProjectRepoErrorCode;
84
- constructor(code: ProjectRepoErrorCode, message: string) {
85
- super(message);
86
- this.name = "ProjectRepoError";
87
- this.code = code;
88
- }
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // Constants
93
- // ---------------------------------------------------------------------------
94
-
95
- export const METADATA_PATH = "metadata.json";
96
- export const CONVERSATIONS_DIR = "conversations";
97
-
98
- const conversationPath = (conversationId: string): string =>
99
- `${CONVERSATIONS_DIR}/${conversationId}.jsonl`;
100
-
101
- // ---------------------------------------------------------------------------
102
- // Pure helpers
103
- // ---------------------------------------------------------------------------
104
-
105
- /** Parse JSONL content; tolerant of blank lines and trailing newline. */
106
- export function parseJsonl(content: string): unknown[] {
107
- return content
108
- .split("\n")
109
- .map((line) => line.trim())
110
- .filter((line) => line.length > 0)
111
- .map((line) => JSON.parse(line) as unknown);
112
- }
113
-
114
- /** Append one JSON line to existing JSONL content (immutable; never mutates input). */
115
- export function appendJsonlLine(prev: string, value: unknown): string {
116
- const base = prev.length > 0 && !prev.endsWith("\n") ? `${prev}\n` : prev;
117
- return `${base}${JSON.stringify(value)}\n`;
118
- }
119
-
120
- /** Well-formed empty metadata — used when a project repo has no metadata yet. */
121
- export function emptyProjectMetadata(projectId: string, name: string): ProjectMetadata {
122
- return {
123
- projectId,
124
- name,
125
- conversations: [],
126
- deployments: [],
127
- };
128
- }
129
-
130
- // ---------------------------------------------------------------------------
131
- // Zod schema (boundary validation)
132
- // ---------------------------------------------------------------------------
133
-
134
- const conversationSummarySchema = z.object({
135
- id: z.string(),
136
- title: z.string(),
137
- createdAt: z.string(),
138
- updatedAt: z.string(),
139
- });
140
-
141
- const deploymentSummarySchema = z.object({
142
- commitSha: z.string(),
143
- commitMessage: z.string(),
144
- commitDate: z.string(),
145
- domain: z.string(),
146
- url: z.string(),
147
- deploymentId: z.string().nullable(),
148
- state: z.enum(["idle", "deploying", "live", "failed"]),
149
- });
150
-
151
- const projectMetadataSchema = z.object({
152
- projectId: z.string(),
153
- name: z.string(),
154
- conversations: z.array(conversationSummarySchema),
155
- deployments: z.array(deploymentSummarySchema),
156
- productionDomain: z.string().optional(),
157
- productionDeploymentId: z.string().optional(),
158
- });
159
-
160
- function normalizeMetadata(meta: ProjectMetadata): ProjectMetadata {
161
- const parsed = projectMetadataSchema.parse(meta);
162
- const out: ProjectMetadata = {
163
- projectId: parsed.projectId,
164
- name: parsed.name,
165
- conversations: parsed.conversations,
166
- deployments: parsed.deployments,
167
- };
168
- if (parsed.productionDomain !== undefined) {
169
- out.productionDomain = parsed.productionDomain;
170
- }
171
- if (parsed.productionDeploymentId !== undefined) {
172
- out.productionDeploymentId = parsed.productionDeploymentId;
173
- }
174
- return out;
175
- }
176
-
177
- // ---------------------------------------------------------------------------
178
- // ProjectRepo
179
- // ---------------------------------------------------------------------------
180
-
181
- export class ProjectRepo {
182
- private readonly host: GitHostPort;
183
- private readonly ownsRepo: OwnsRepoPredicate;
184
-
185
- constructor(deps: ProjectRepoDeps) {
186
- this.host = deps.host;
187
- // Default-deny: if no ownership predicate is wired, all access is denied.
188
- this.ownsRepo = deps.ownsRepo ?? (() => false);
189
- }
190
-
191
- private async authorize(tenantId: string, repoId: string): Promise<void> {
192
- if (typeof tenantId !== "string" || tenantId.trim().length === 0) {
193
- throw new ProjectRepoError("tenant_required", "tenantId is required");
194
- }
195
- const owns = await this.ownsRepo(tenantId, repoId);
196
- if (!owns) {
197
- throw new ProjectRepoError(
198
- "tenant_denied",
199
- `tenant ${tenantId} may not access repo ${repoId}`,
200
- );
201
- }
202
- }
203
-
204
- private async defaultBranch(repoId: string): Promise<string> {
205
- return this.host.getDefaultBranch(repoId);
206
- }
207
-
208
- async readMetadata(tenantId: string, repoId: string): Promise<ProjectMetadata> {
209
- await this.authorize(tenantId, repoId);
210
- const raw = await this.host.readFile(repoId, METADATA_PATH);
211
- if (raw === null) {
212
- return emptyProjectMetadata("", "");
213
- }
214
- let parsedJson: unknown;
215
- try {
216
- parsedJson = JSON.parse(raw);
217
- } catch {
218
- throw new ProjectRepoError("metadata_malformed", `${METADATA_PATH} is not valid JSON`);
219
- }
220
- const result = projectMetadataSchema.safeParse(parsedJson);
221
- if (!result.success) {
222
- throw new ProjectRepoError(
223
- "metadata_malformed",
224
- `${METADATA_PATH} does not match the project metadata schema`,
225
- );
226
- }
227
- return normalizeMetadata(result.data);
228
- }
229
-
230
- async writeMetadata(tenantId: string, repoId: string, meta: ProjectMetadata): Promise<CommitRef> {
231
- await this.authorize(tenantId, repoId);
232
- const normalized = normalizeMetadata(meta);
233
- const branch = await this.defaultBranch(repoId);
234
- return this.host.writeCommit(
235
- repoId,
236
- branch,
237
- { [METADATA_PATH]: `${JSON.stringify(normalized, null, 2)}\n` },
238
- "chore: update project metadata",
239
- );
240
- }
241
-
242
- async appendConversationMessage(
243
- tenantId: string,
244
- repoId: string,
245
- conversationId: string,
246
- message: unknown,
247
- ): Promise<CommitRef> {
248
- await this.authorize(tenantId, repoId);
249
- const path = conversationPath(conversationId);
250
- const existing = (await this.host.readFile(repoId, path)) ?? "";
251
- const next = appendJsonlLine(existing, message);
252
- const branch = await this.defaultBranch(repoId);
253
- // One commit per call = one commit per turn.
254
- return this.host.writeCommit(
255
- repoId,
256
- branch,
257
- { [path]: next },
258
- `chore: append message to ${conversationId}`,
259
- );
260
- }
261
-
262
- async readConversationMessages(
263
- tenantId: string,
264
- repoId: string,
265
- conversationId: string,
266
- ): Promise<unknown[]> {
267
- await this.authorize(tenantId, repoId);
268
- const raw = await this.host.readFile(repoId, conversationPath(conversationId));
269
- if (raw === null) return [];
270
- return parseJsonl(raw);
271
- }
272
-
273
- async restoreAt(
274
- tenantId: string,
275
- repoId: string,
276
- sha: string,
277
- ): Promise<{ metadata: ProjectMetadata }> {
278
- await this.authorize(tenantId, repoId);
279
- const raw = await this.host.readFile(repoId, METADATA_PATH, sha);
280
- if (raw === null) {
281
- return { metadata: emptyProjectMetadata("", "") };
282
- }
283
- let parsedJson: unknown;
284
- try {
285
- parsedJson = JSON.parse(raw);
286
- } catch {
287
- throw new ProjectRepoError(
288
- "metadata_malformed",
289
- `${METADATA_PATH} at ${sha} is not valid JSON`,
290
- );
291
- }
292
- const result = projectMetadataSchema.safeParse(parsedJson);
293
- if (!result.success) {
294
- throw new ProjectRepoError(
295
- "metadata_malformed",
296
- `${METADATA_PATH} at ${sha} does not match the project metadata schema`,
297
- );
298
- }
299
- return { metadata: normalizeMetadata(result.data) };
300
- }
301
-
302
- async historyFromCommits(
303
- tenantId: string,
304
- repoId: string,
305
- limit?: number | undefined,
306
- ): Promise<CommitRef[]> {
307
- await this.authorize(tenantId, repoId);
308
- const branch = await this.defaultBranch(repoId);
309
- return this.host.listCommits(repoId, branch, limit);
310
- }
311
- }
package/src/protocol.ts DELETED
@@ -1,159 +0,0 @@
1
- /**
2
- * App-server protocol contract (WRAP — capability #5).
3
- *
4
- * Faithful re-expression of the upstream JSON-RPC method registry, the
5
- * serialization-scope concurrency model, server-initiated approval requests,
6
- * and the notification event stream.
7
- *
8
- * Multi-tenant adaptation (non-negotiable): every serialization scope is
9
- * additionally keyed by `tenantId`. Upstream `Thread { thread_id }` becomes
10
- * `{ tenantId, threadId }` — natural per-tenant ordering + isolation for free.
11
- * This is the only coupling seam to an optional decoupled kernel sidecar
12
- * (Track B); transport (WS/SSE/stdio) is pluggable and not defined here.
13
- */
14
-
15
- import { z } from "zod";
16
-
17
- // ── Serialization scope (concurrency control) ────────────────────────────────
18
-
19
- /**
20
- * Requests sharing a scope are serialized; distinct scopes run in parallel.
21
- * `tenantId` is mandatory on every scope — cross-tenant requests can never
22
- * share a scope.
23
- */
24
- export type SerializationScope =
25
- | { readonly tenantId: string; readonly kind: "none" }
26
- | { readonly tenantId: string; readonly kind: "global"; readonly key: string }
27
- | { readonly tenantId: string; readonly kind: "global_shared_read"; readonly key: string }
28
- | { readonly tenantId: string; readonly kind: "thread"; readonly threadId: string }
29
- | { readonly tenantId: string; readonly kind: "command_exec"; readonly processId: string }
30
- | { readonly tenantId: string; readonly kind: "mcp_oauth"; readonly server: string };
31
-
32
- /** Stable string key used to coalesce requests into the same serial lane. */
33
- export function scopeKey(scope: SerializationScope): string {
34
- switch (scope.kind) {
35
- case "none":
36
- return `t:${scope.tenantId}|none:${cryptoRandom()}`;
37
- case "global":
38
- case "global_shared_read":
39
- return `t:${scope.tenantId}|${scope.kind}:${scope.key}`;
40
- case "thread":
41
- return `t:${scope.tenantId}|thread:${scope.threadId}`;
42
- case "command_exec":
43
- return `t:${scope.tenantId}|cmd:${scope.processId}`;
44
- case "mcp_oauth":
45
- return `t:${scope.tenantId}|mcpoauth:${scope.server}`;
46
- }
47
- }
48
-
49
- function cryptoRandom(): string {
50
- return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
51
- }
52
-
53
- // ── Client -> server method registry ─────────────────────────────────────────
54
-
55
- /** How a method's scope is derived from its params. */
56
- export type ScopeRule =
57
- | { readonly kind: "none" }
58
- | { readonly kind: "global"; readonly key: string }
59
- | { readonly kind: "thread" };
60
-
61
- export interface MethodSpec {
62
- /** Namespaced `domain/verb` wire name. */
63
- readonly method: string;
64
- readonly scope: ScopeRule;
65
- readonly experimental?: boolean;
66
- }
67
-
68
- /**
69
- * The faithful subset of the upstream registry relevant to a multi-tenant web
70
- * product. `thread`-scoped methods derive `{ tenantId, threadId }`.
71
- */
72
- export const METHOD_REGISTRY = {
73
- initialize: { method: "initialize", scope: { kind: "none" } },
74
- threadStart: { method: "thread/start", scope: { kind: "none" } },
75
- threadResume: { method: "thread/resume", scope: { kind: "thread" } },
76
- threadFork: { method: "thread/fork", scope: { kind: "thread" } },
77
- threadArchive: { method: "thread/archive", scope: { kind: "thread" } },
78
- threadCompactStart: { method: "thread/compact/start", scope: { kind: "thread" } },
79
- threadRollback: { method: "thread/rollback", scope: { kind: "thread" } },
80
- turnStart: { method: "turn/start", scope: { kind: "thread" } },
81
- turnSteer: { method: "turn/steer", scope: { kind: "thread" } },
82
- turnInterrupt: { method: "turn/interrupt", scope: { kind: "thread" } },
83
- modelList: { method: "model/list", scope: { kind: "none" } },
84
- } as const satisfies Record<string, MethodSpec>;
85
-
86
- export type MethodName = (typeof METHOD_REGISTRY)[keyof typeof METHOD_REGISTRY]["method"];
87
-
88
- /** Resolve a method's runtime scope from tenant + params. */
89
- export function resolveScope(
90
- spec: MethodSpec,
91
- tenantId: string,
92
- params: { threadId?: string },
93
- ): SerializationScope {
94
- switch (spec.scope.kind) {
95
- case "none":
96
- return { tenantId, kind: "none" };
97
- case "global":
98
- return { tenantId, kind: "global", key: spec.scope.key };
99
- case "thread": {
100
- if (!params.threadId) {
101
- throw new Error(`Method ${spec.method} requires a threadId for scoping`);
102
- }
103
- return { tenantId, kind: "thread", threadId: params.threadId };
104
- }
105
- }
106
- }
107
-
108
- // ── Server -> client requests (approval / elicitation) ───────────────────────
109
-
110
- /** Approvals are server-initiated RPC, not side channels. */
111
- export type ServerRequest =
112
- | {
113
- readonly type: "command_execution.request_approval";
114
- readonly requestId: string;
115
- readonly command: string;
116
- }
117
- | {
118
- readonly type: "file_change.request_approval";
119
- readonly requestId: string;
120
- readonly paths: readonly string[];
121
- }
122
- | {
123
- readonly type: "permissions.request_approval";
124
- readonly requestId: string;
125
- readonly summary: string;
126
- }
127
- | {
128
- readonly type: "mcp.elicitation";
129
- readonly requestId: string;
130
- readonly server: string;
131
- readonly schema: unknown;
132
- };
133
-
134
- // ── Server -> client notifications (event stream) ────────────────────────────
135
-
136
- export const NOTIFICATIONS = [
137
- "thread/started",
138
- "turn/started",
139
- "turn/completed",
140
- "turn/failed",
141
- "item/started",
142
- "item/updated",
143
- "item/completed",
144
- "model/rerouted",
145
- "thread/compacted",
146
- ] as const;
147
- export type NotificationName = (typeof NOTIFICATIONS)[number];
148
-
149
- // ── Boundary validation for the universal envelope ───────────────────────────
150
-
151
- export const requestEnvelopeSchema = z.object({
152
- jsonrpc: z.literal("2.0"),
153
- id: z.union([z.string(), z.number()]),
154
- method: z.string(),
155
- /** tenantId is mandatory at the envelope boundary — fail closed otherwise. */
156
- tenantId: z.string().min(1),
157
- params: z.unknown().optional(),
158
- });
159
- export type RequestEnvelope = z.infer<typeof requestEnvelopeSchema>;