Skip to content

Commit c50c671

Browse files
committed
refactor(agent-runtime): tseng skills + result types + protected trpc
- Layered skills/ module: domain (skill-name/path, frontmatter, branch-timestamp), infra ports (github-rest, git-protocol, local-skill-repo), services (install/scan/publish + facade), compose.ts + index.ts - Contract: skills router on protectedProcedure; errorFormatter lifts upstream envelope to data.upstream - Files module migrated to Result<T, FilesDomainError> + protected - server.ts: per-request createContext reads bearer; deletes hand- rolled /api/skills/* HTTP routes - api-server: typed @trpc/client to agent-runtime; instance proxy swaps user JWT for agent bearer after ownership check - Drop skills/files unit tests (per spec deferral) - tseng vocabulary: Skills bounded context; adoption.md notes Result applied for agent-runtime Signed-off-by: Tomas Weiss <tomas.weiss2@gmail.com>
1 parent 7d94937 commit c50c671

34 files changed

Lines changed: 1745 additions & 2161 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { FilesService } from "./modules/files/types.js";
2+
import type { SkillsService } from "./modules/skills/types.js";
3+
4+
export interface AgentAuth {
5+
agentCaller: true;
6+
}
27

38
export interface AgentRuntimeContext {
9+
auth: AgentAuth | null;
410
files: FilesService;
11+
skills: SkillsService;
512
}
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
export type { AppRouter } from "./router.js";
2-
export type { AgentRuntimeContext } from "./context.js";
3-
export type { FilesService } from "./modules/files/types.js";
2+
export type { AgentAuth, AgentRuntimeContext } from "./context.js";
3+
export type { Result } from "./result.js";
4+
export { ok, err } from "./result.js";
5+
export type {
6+
FileReadResult,
7+
FileWriteOk,
8+
FilesDomainError,
9+
FilesService,
10+
} from "./modules/files/types.js";
11+
export type {
12+
GitHubErrorBody,
13+
InstallSkillInput,
14+
InstallSkillResult,
15+
ListLocalSkillsInput,
16+
LocalSkill,
17+
LocalSkillFile,
18+
PublishSkillInput,
19+
PublishSkillResult,
20+
ReadLocalSkillInput,
21+
ReadLocalSkillResult,
22+
ScanSkillSourceInput,
23+
ScannedSkill,
24+
SkillsDomainError,
25+
SkillsService,
26+
UninstallSkillInput,
27+
} from "./modules/skills/types.js";
Lines changed: 61 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,95 @@
11
import { TRPCError } from "@trpc/server";
22
import { z } from "zod";
3-
import { t } from "../../trpc.js";
3+
import { protectedProcedure, t } from "../../trpc.js";
4+
import type { FilesDomainError } from "./types.js";
45

56
const pathSchema = z.string().min(1);
67

8+
function toTrpcError(error: FilesDomainError): TRPCError {
9+
switch (error.kind) {
10+
case "Forbidden":
11+
return new TRPCError({ code: "FORBIDDEN", message: error.reason });
12+
case "NotFound":
13+
return new TRPCError({ code: "NOT_FOUND" });
14+
case "Conflict":
15+
return new TRPCError({
16+
code: "CONFLICT",
17+
message: "file changed on disk",
18+
cause: { currentMtimeMs: error.currentMtimeMs },
19+
});
20+
case "AlreadyExists":
21+
return new TRPCError({ code: "CONFLICT", message: "path already exists" });
22+
case "PayloadTooLarge":
23+
return new TRPCError({ code: "PAYLOAD_TOO_LARGE", message: error.detail });
24+
}
25+
}
26+
727
export const filesRouter = t.router({
8-
tree: t.procedure.query(({ ctx }) => ({
28+
tree: protectedProcedure.query(({ ctx }) => ({
929
entries: ctx.files.buildTree(),
1030
})),
1131

12-
read: t.procedure
32+
read: protectedProcedure
1333
.input(z.object({ path: pathSchema }))
1434
.query(async ({ ctx, input }) => {
1535
const result = await ctx.files.readFileSafe(input.path);
16-
if (!result) {
17-
throw new TRPCError({ code: "NOT_FOUND" });
18-
}
19-
return result;
36+
if (!result.ok) throw toTrpcError(result.error);
37+
return result.value;
2038
}),
2139

22-
write: t.procedure
40+
write: protectedProcedure
2341
.input(z.object({
2442
path: pathSchema,
2543
content: z.string(),
2644
expectedMtimeMs: z.number().optional(),
2745
}))
2846
.mutation(async ({ ctx, input }) => {
29-
try {
30-
const result = await ctx.files.writeFileSafe(
31-
input.path,
32-
input.content,
33-
input.expectedMtimeMs,
34-
);
35-
if ("conflict" in result) {
36-
throw new TRPCError({
37-
code: "CONFLICT",
38-
message: "file changed on disk",
39-
cause: { currentMtimeMs: result.currentMtimeMs },
40-
});
41-
}
42-
return { mtimeMs: result.mtimeMs };
43-
} catch (err) {
44-
if (err instanceof TRPCError) throw err;
45-
throw new TRPCError({ code: "FORBIDDEN", message: (err as Error).message });
46-
}
47+
const result = await ctx.files.writeFileSafe(
48+
input.path,
49+
input.content,
50+
input.expectedMtimeMs,
51+
);
52+
if (!result.ok) throw toTrpcError(result.error);
53+
return { mtimeMs: result.value.mtimeMs };
4754
}),
4855

49-
create: t.procedure
56+
create: protectedProcedure
5057
.input(z.object({ path: pathSchema, content: z.string() }))
5158
.mutation(async ({ ctx, input }) => {
52-
try {
53-
const result = await ctx.files.createFileSafe(input.path, input.content);
54-
if ("exists" in result) {
55-
throw new TRPCError({ code: "CONFLICT", message: "path already exists" });
56-
}
57-
return { mtimeMs: result.mtimeMs };
58-
} catch (err) {
59-
if (err instanceof TRPCError) throw err;
60-
throw new TRPCError({ code: "FORBIDDEN", message: (err as Error).message });
61-
}
59+
const result = await ctx.files.createFileSafe(input.path, input.content);
60+
if (!result.ok) throw toTrpcError(result.error);
61+
return { mtimeMs: result.value.mtimeMs };
6262
}),
6363

64-
mkdir: t.procedure
64+
mkdir: protectedProcedure
6565
.input(z.object({ path: pathSchema }))
6666
.mutation(async ({ ctx, input }) => {
67-
try {
68-
const result = await ctx.files.mkdirSafe(input.path);
69-
if ("exists" in result) {
70-
throw new TRPCError({ code: "CONFLICT", message: "path exists and is not a directory" });
71-
}
72-
return { ok: true as const };
73-
} catch (err) {
74-
if (err instanceof TRPCError) throw err;
75-
throw new TRPCError({ code: "FORBIDDEN", message: (err as Error).message });
76-
}
67+
const result = await ctx.files.mkdirSafe(input.path);
68+
if (!result.ok) throw toTrpcError(result.error);
69+
return { ok: true as const };
7770
}),
7871

79-
rename: t.procedure
72+
rename: protectedProcedure
8073
.input(z.object({
8174
from: pathSchema,
8275
to: pathSchema,
8376
overwrite: z.boolean().optional(),
8477
}))
8578
.mutation(async ({ ctx, input }) => {
86-
try {
87-
const result = await ctx.files.renameSafe(input.from, input.to, input.overwrite ?? false);
88-
if ("exists" in result) {
89-
throw new TRPCError({ code: "CONFLICT", message: "destination already exists" });
90-
}
91-
return { ok: true as const };
92-
} catch (err) {
93-
if (err instanceof TRPCError) throw err;
94-
throw new TRPCError({ code: "FORBIDDEN", message: (err as Error).message });
95-
}
79+
const result = await ctx.files.renameSafe(input.from, input.to, input.overwrite ?? false);
80+
if (!result.ok) throw toTrpcError(result.error);
81+
return { ok: true as const };
9682
}),
9783

98-
remove: t.procedure
84+
remove: protectedProcedure
9985
.input(z.object({ path: pathSchema }))
10086
.mutation(async ({ ctx, input }) => {
101-
try {
102-
await ctx.files.deleteSafe(input.path);
103-
return { ok: true as const };
104-
} catch (err) {
105-
if (err instanceof TRPCError) throw err;
106-
throw new TRPCError({ code: "FORBIDDEN", message: (err as Error).message });
107-
}
87+
const result = await ctx.files.deleteSafe(input.path);
88+
if (!result.ok) throw toTrpcError(result.error);
89+
return { ok: true as const };
10890
}),
10991

110-
upload: t.procedure
92+
upload: protectedProcedure
11193
.input(z.object({
11294
path: pathSchema,
11395
contentBase64: z.string(),
@@ -118,27 +100,16 @@ export const filesRouter = t.router({
118100
overwrite: z.boolean().optional(),
119101
}))
120102
.mutation(async ({ ctx, input }) => {
121-
try {
122-
const result = await ctx.files.uploadFileSafe(
123-
input.path,
124-
input.contentBase64,
125-
input.overwrite ?? false,
126-
);
127-
if ("exists" in result) {
128-
throw new TRPCError({ code: "CONFLICT", message: "path already exists" });
129-
}
130-
return {
131-
mtimeMs: result.mtimeMs,
132-
absolutePath: result.absolutePath,
133-
contentType: input.contentType,
134-
};
135-
} catch (err) {
136-
if (err instanceof TRPCError) throw err;
137-
const msg = (err as Error).message;
138-
throw new TRPCError({
139-
code: /too large/i.test(msg) ? "PAYLOAD_TOO_LARGE" : "FORBIDDEN",
140-
message: msg,
141-
});
142-
}
103+
const result = await ctx.files.uploadFileSafe(
104+
input.path,
105+
input.contentBase64,
106+
input.overwrite ?? false,
107+
);
108+
if (!result.ok) throw toTrpcError(result.error);
109+
return {
110+
mtimeMs: result.value.mtimeMs,
111+
absolutePath: result.value.absolutePath,
112+
contentType: input.contentType,
113+
};
143114
}),
144115
});
Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import type { Result } from "../../result.js";
2+
13
export interface FileReadResult {
24
path: string;
3-
content?: string; // UTF-8 for text, base64 for binary
4-
binary?: boolean; // true when content is base64-encoded or file exceeds size limit
5-
mimeType?: string; // detected MIME type (absent when file exceeds size limit)
6-
mtimeMs?: number; // mtime at read; absent when file exceeds size limit
5+
content?: string;
6+
binary?: boolean;
7+
mimeType?: string;
8+
mtimeMs?: number;
79
}
810

911
export interface FileWriteOk {
@@ -13,46 +15,41 @@ export interface FileWriteOk {
1315
absolutePath?: string;
1416
}
1517

16-
export interface FileConflict {
17-
conflict: true;
18-
currentMtimeMs: number;
19-
}
20-
21-
export interface PathExists {
22-
exists: true;
23-
}
18+
export type FilesDomainError =
19+
| { kind: "Forbidden"; reason: string }
20+
| { kind: "NotFound"; path: string }
21+
| { kind: "Conflict"; currentMtimeMs: number }
22+
| { kind: "AlreadyExists"; path: string }
23+
| { kind: "PayloadTooLarge"; detail: string };
2424

2525
export interface FilesService {
2626
buildTree: () => { path: string; type: "file" | "dir" }[];
27-
readFileSafe: (rel: string) => Promise<FileReadResult | null>;
28-
/** Overwrite an existing file. Returns conflict when expectedMtimeMs is
27+
readFileSafe: (rel: string) => Promise<Result<FileReadResult, FilesDomainError>>;
28+
/** Overwrite an existing file. Errors with Conflict when expectedMtimeMs is
2929
* provided and the file was modified in the meantime. */
3030
writeFileSafe: (
3131
rel: string,
3232
content: string,
3333
expectedMtimeMs?: number,
34-
) => Promise<FileWriteOk | FileConflict>;
35-
/** Create a new file. Fails with `{exists: true}` when the path already
36-
* exists. Auto-creates missing parent directories. */
37-
createFileSafe: (rel: string, content: string) => Promise<FileWriteOk | PathExists>;
38-
/** Create a directory (recursive mkdir). Returns `{exists: true}` if the
39-
* path already exists and is not a directory. */
40-
mkdirSafe: (rel: string) => Promise<{ ok: true } | PathExists>;
41-
/** Move/rename a file or directory. Returns `{exists: true}` when the
34+
) => Promise<Result<FileWriteOk, FilesDomainError>>;
35+
/** Create a new file. Errors with AlreadyExists when the path is taken.
36+
* Auto-creates missing parent directories. */
37+
createFileSafe: (rel: string, content: string) => Promise<Result<FileWriteOk, FilesDomainError>>;
38+
/** Create a directory (recursive mkdir). */
39+
mkdirSafe: (rel: string) => Promise<Result<{ ok: true }, FilesDomainError>>;
40+
/** Move/rename a file or directory. Errors with AlreadyExists when the
4241
* destination exists and overwrite is false. */
4342
renameSafe: (
4443
from: string,
4544
to: string,
4645
overwrite: boolean,
47-
) => Promise<{ ok: true } | PathExists>;
48-
/** Remove a file or directory (recursive for dirs). */
49-
deleteSafe: (rel: string) => Promise<{ ok: true }>;
50-
/** Write a binary payload (base64-encoded) to disk. When `overwrite` is
51-
* false and the destination exists, returns `{exists: true}` without
52-
* clobbering. Intended for UI uploads where the client has no prior mtime. */
46+
) => Promise<Result<{ ok: true }, FilesDomainError>>;
47+
deleteSafe: (rel: string) => Promise<Result<{ ok: true }, FilesDomainError>>;
48+
/** Write a binary payload (base64-encoded) to disk. Intended for UI
49+
* uploads where the client has no prior mtime. */
5350
uploadFileSafe: (
5451
rel: string,
5552
base64: string,
5653
overwrite: boolean,
57-
) => Promise<FileWriteOk | PathExists>;
54+
) => Promise<Result<FileWriteOk, FilesDomainError>>;
5855
}

0 commit comments

Comments
 (0)