mirab 0.0.0 → 0.2.0
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/LICENSE +201 -0
- package/README.md +223 -3
- package/package.json +56 -7
- package/src/agent/approved.ts +261 -0
- package/src/agent/define.ts +235 -0
- package/src/agent/guest.ts +138 -0
- package/src/agent/init.ts +150 -0
- package/src/agent/manifest.ts +141 -0
- package/src/agent/modules.ts +23 -0
- package/src/agent/runtime.ts +239 -0
- package/src/agent-profiles.ts +327 -0
- package/src/api/bootstrap-contract.ts +95 -0
- package/src/api/channel-contract.ts +19 -0
- package/src/api/client.ts +100 -0
- package/src/api/contract.ts +2454 -0
- package/src/api/events-contract.ts +103 -0
- package/src/api/import-contract.ts +22 -0
- package/src/api/orchestrator-contract.ts +73 -0
- package/src/api/runner-contract.ts +25 -0
- package/src/api/scopes.ts +41 -0
- package/src/api/sessions-contract.ts +71 -0
- package/src/api/settings-contract.ts +50 -0
- package/src/channel/bridge.ts +97 -0
- package/src/channel/local.ts +154 -0
- package/src/channel/state.ts +11 -0
- package/src/channel/wire.ts +104 -0
- package/src/dev-sync.ts +206 -0
- package/src/dev.ts +453 -0
- package/src/events-tail.ts +75 -0
- package/src/images.ts +498 -0
- package/src/index.ts +21 -0
- package/src/invocation.ts +34 -0
- package/src/isolation/anchored-fs.ts +284 -0
- package/src/isolation/colima.ts +159 -0
- package/src/isolation/container.ts +189 -0
- package/src/isolation/firecracker.ts +414 -0
- package/src/isolation/guest-protocol.json +1 -0
- package/src/isolation/host-user.ts +247 -0
- package/src/isolation/image.ts +336 -0
- package/src/isolation/index.ts +477 -0
- package/src/isolation/multipass.ts +541 -0
- package/src/isolation/network.ts +167 -0
- package/src/isolation/policy.ts +86 -0
- package/src/isolation/session.ts +385 -0
- package/src/isolation/sync.ts +329 -0
- package/src/isolation/vz-network-setup.ts +247 -0
- package/src/isolation/vz.ts +312 -0
- package/src/mirab-main.ts +536 -0
- package/src/mirab.ts +13 -0
- package/src/runner/define.ts +187 -0
- package/src/runner/image.ts +239 -0
- package/src/runner/index.ts +37 -0
- package/src/runner/serve.ts +491 -0
- package/src/runner/toolsets.ts +205 -0
- package/src/spawn-adapters/claude.ts +112 -0
- package/src/spawn-adapters/codex-logs.ts +130 -0
- package/src/spawn-adapters/codex-notify.ts +73 -0
- package/src/spawn-adapters/codex.ts +89 -0
- package/src/spawn-adapters/index.ts +40 -0
- package/src/spawn-adapters/types.ts +55 -0
- package/src/spawn-attach.ts +243 -0
- package/src/spawn-binary.ts +90 -0
- package/src/spawn-bridge.ts +504 -0
- package/src/spawn-bundle.ts +16 -0
- package/src/spawn-detach.ts +301 -0
- package/src/spawn-events.ts +35 -0
- package/src/spawn-hook.ts +86 -0
- package/src/spawn-input.ts +107 -0
- package/src/spawn-native.ts +49 -0
- package/src/spawn-nudge-worker.ts +61 -0
- package/src/spawn-nudge.ts +446 -0
- package/src/spawn-output.ts +45 -0
- package/src/spawn-queue.ts +135 -0
- package/src/spawn-quiet.ts +69 -0
- package/src/spawn-report.ts +114 -0
- package/src/spawn-sessions.ts +166 -0
- package/src/spawn-vm.ts +356 -0
- package/src/spawn-worker.ts +766 -0
- package/src/spawn.ts +468 -0
- package/src/station/config.ts +89 -0
- package/src/station/gateway.ts +94 -0
- package/src/station/vault.ts +96 -0
- package/src/test-cli.ts +90 -0
- package/src/tools.ts +3 -0
- package/types/agent/define.d.ts +76 -0
- package/types/agent/manifest.d.ts +53 -0
- package/types/agent/modules.d.ts +1 -0
- package/types/images.d.ts +124 -0
- package/types/index.d.ts +6 -0
- package/types/isolation/colima.d.ts +37 -0
- package/types/isolation/container.d.ts +25 -0
- package/types/isolation/firecracker.d.ts +55 -0
- package/types/isolation/host-user.d.ts +47 -0
- package/types/isolation/image.d.ts +158 -0
- package/types/isolation/index.d.ts +128 -0
- package/types/isolation/network.d.ts +33 -0
- package/types/isolation/policy.d.ts +29 -0
- package/types/isolation/session.d.ts +99 -0
- package/types/isolation/vz.d.ts +63 -0
- package/types/runner/define.d.ts +101 -0
- package/types/runner/image.d.ts +52 -0
- package/types/runner/index.d.ts +13 -0
- package/types/runner/serve.d.ts +69 -0
- package/types/runner/toolsets.d.ts +35 -0
- package/types/tools.d.ts +2 -0
- package/index.js +0 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmod,
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
open,
|
|
6
|
+
readdir,
|
|
7
|
+
readFile,
|
|
8
|
+
rename,
|
|
9
|
+
unlink,
|
|
10
|
+
} from 'node:fs/promises'
|
|
11
|
+
import { constants } from 'node:fs'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import { isAbsolute, join, resolve } from 'node:path'
|
|
14
|
+
import { randomBytes } from 'node:crypto'
|
|
15
|
+
import { Command } from 'commander'
|
|
16
|
+
import { z } from 'zod'
|
|
17
|
+
import { nudgeSpec } from './spawn-nudge'
|
|
18
|
+
|
|
19
|
+
// Runtime settings only. Scoped tokens stay in the existing identity store.
|
|
20
|
+
const nameSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,23}$/)
|
|
21
|
+
const text = z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.refine((s) => !s.includes('\0'))
|
|
25
|
+
/** `<event>.<action>[:<label>]` against the forge event stream (gild-site#55). */
|
|
26
|
+
export const triggerPattern = /^([a-z][a-z_]*)\.([a-z][a-z_]*)(?::([^\n]+))?$/
|
|
27
|
+
const triggerSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(
|
|
30
|
+
triggerPattern,
|
|
31
|
+
'Use <event>.<action>[:<label>], e.g. issues.labeled:triage',
|
|
32
|
+
)
|
|
33
|
+
export type AgentTrigger = { event: string; action: string; label?: string }
|
|
34
|
+
/** Split a validated trigger spec into its parts; null when the grammar fails. */
|
|
35
|
+
export function parseTrigger(spec: string): AgentTrigger | null {
|
|
36
|
+
const match = spec.match(triggerPattern)
|
|
37
|
+
if (!match) return null
|
|
38
|
+
return {
|
|
39
|
+
event: match[1],
|
|
40
|
+
action: match[2],
|
|
41
|
+
...(match[3] !== undefined ? { label: match[3] } : {}),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const profileSchema = z.strictObject({
|
|
45
|
+
name: nameSchema,
|
|
46
|
+
runtime: text,
|
|
47
|
+
model: text.optional(),
|
|
48
|
+
effort: text.optional(),
|
|
49
|
+
directory: text.refine(isAbsolute),
|
|
50
|
+
args: z.array(z.string().refine((s) => !s.includes('\0'))).default([]),
|
|
51
|
+
env: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).optional(),
|
|
52
|
+
channels: z
|
|
53
|
+
.array(z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/))
|
|
54
|
+
.optional(),
|
|
55
|
+
nudges: z.array(nudgeSpec).optional(),
|
|
56
|
+
on: z.array(triggerSchema).optional(),
|
|
57
|
+
})
|
|
58
|
+
export type AgentProfile = z.infer<typeof profileSchema>
|
|
59
|
+
export const profilesDirectory = () => join(homedir(), '.mirab', 'agents')
|
|
60
|
+
function profilePath(name: string) {
|
|
61
|
+
if (!nameSchema.safeParse(name).success)
|
|
62
|
+
throw Error(
|
|
63
|
+
'Agent profile name must be 1–24 letters, digits, underscores or hyphens',
|
|
64
|
+
)
|
|
65
|
+
return join(profilesDirectory(), name + '.json')
|
|
66
|
+
}
|
|
67
|
+
async function privateDirectory() {
|
|
68
|
+
for (const path of [join(homedir(), '.mirab'), profilesDirectory()]) {
|
|
69
|
+
await mkdir(path, { mode: 0o700 }).catch((e: NodeJS.ErrnoException) => {
|
|
70
|
+
if (e.code !== 'EEXIST') throw e
|
|
71
|
+
})
|
|
72
|
+
const info = await lstat(path)
|
|
73
|
+
if (
|
|
74
|
+
!info.isDirectory() ||
|
|
75
|
+
info.isSymbolicLink() ||
|
|
76
|
+
info.uid !== process.getuid?.()
|
|
77
|
+
)
|
|
78
|
+
throw Error('Unsafe local agent profile directory')
|
|
79
|
+
await chmod(path, 0o700)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function nudgeError(issues: z.core.$ZodIssue[]) {
|
|
83
|
+
const issue = issues.find((i) => i.path[0] === 'nudges')
|
|
84
|
+
return issue && `Invalid nudge rule: ${issue.message}`
|
|
85
|
+
}
|
|
86
|
+
export function parseProfile(value: unknown): AgentProfile {
|
|
87
|
+
const result = profileSchema.safeParse(value)
|
|
88
|
+
if (!result.success)
|
|
89
|
+
throw Error(
|
|
90
|
+
nudgeError(result.error.issues) ??
|
|
91
|
+
'Invalid agent profile: only name, runtime, model, effort, directory, args, env, channels, on and nudges are allowed; credentials belong in the identity store' +
|
|
92
|
+
(result.error.issues[0]
|
|
93
|
+
? ` (${result.error.issues[0].path.join('.') || 'profile'}: ${result.error.issues[0].message})`
|
|
94
|
+
: ''),
|
|
95
|
+
)
|
|
96
|
+
return result.data
|
|
97
|
+
}
|
|
98
|
+
export async function loadProfile(name: string): Promise<AgentProfile> {
|
|
99
|
+
const path = profilePath(name)
|
|
100
|
+
await privateDirectory()
|
|
101
|
+
const file = await open(
|
|
102
|
+
path,
|
|
103
|
+
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
104
|
+
).catch((e: NodeJS.ErrnoException) => {
|
|
105
|
+
if (e.code === 'ENOENT')
|
|
106
|
+
throw Error(`No local agent profile ${name}; use mirab agent add ${name}`)
|
|
107
|
+
throw e
|
|
108
|
+
})
|
|
109
|
+
try {
|
|
110
|
+
const info = await file.stat()
|
|
111
|
+
if (
|
|
112
|
+
!info.isFile() ||
|
|
113
|
+
info.uid !== process.getuid?.() ||
|
|
114
|
+
info.size > 64 * 1024
|
|
115
|
+
)
|
|
116
|
+
throw Error('Unsafe local agent profile file')
|
|
117
|
+
await file.chmod(0o600)
|
|
118
|
+
let value: unknown
|
|
119
|
+
try {
|
|
120
|
+
value = JSON.parse(await file.readFile('utf8'))
|
|
121
|
+
} catch {
|
|
122
|
+
throw Error('Invalid agent profile JSON')
|
|
123
|
+
}
|
|
124
|
+
const profile = parseProfile(value)
|
|
125
|
+
if (profile.name !== name)
|
|
126
|
+
throw Error('Agent profile name differs from its filename')
|
|
127
|
+
return profile
|
|
128
|
+
} finally {
|
|
129
|
+
await file.close()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function saveProfile(value: unknown, create = false) {
|
|
133
|
+
const profile = parseProfile(value)
|
|
134
|
+
const path = profilePath(profile.name)
|
|
135
|
+
await privateDirectory()
|
|
136
|
+
const body = JSON.stringify(profile, null, 2) + '\n'
|
|
137
|
+
if (Buffer.byteLength(body) > 64 * 1024)
|
|
138
|
+
throw Error('Agent profile exceeds 64 KiB')
|
|
139
|
+
if (create) {
|
|
140
|
+
const file = await open(path, 'wx', 0o600).catch(
|
|
141
|
+
(e: NodeJS.ErrnoException) => {
|
|
142
|
+
if (e.code === 'EEXIST')
|
|
143
|
+
throw Error(
|
|
144
|
+
`Agent profile ${profile.name} already exists; use mirab agent edit`,
|
|
145
|
+
)
|
|
146
|
+
throw e
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
try {
|
|
150
|
+
await file.writeFile(body)
|
|
151
|
+
} finally {
|
|
152
|
+
await file.close()
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
await loadProfile(profile.name)
|
|
156
|
+
const temp = path + '.' + randomBytes(6).toString('hex') + '.tmp'
|
|
157
|
+
try {
|
|
158
|
+
const file = await open(temp, 'wx', 0o600)
|
|
159
|
+
try {
|
|
160
|
+
await file.writeFile(body)
|
|
161
|
+
} finally {
|
|
162
|
+
await file.close()
|
|
163
|
+
}
|
|
164
|
+
await rename(temp, path)
|
|
165
|
+
} finally {
|
|
166
|
+
await unlink(temp).catch(() => {})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export async function resolveProfile(name: string, extra: string[]) {
|
|
171
|
+
const profile = await loadProfile(name)
|
|
172
|
+
const { stat } = await import('node:fs/promises')
|
|
173
|
+
const directory = await stat(profile.directory).catch(() => null)
|
|
174
|
+
if (!directory?.isDirectory())
|
|
175
|
+
throw Error(
|
|
176
|
+
`Agent profile ${name} directory does not exist or is not a directory: ${profile.directory}`,
|
|
177
|
+
)
|
|
178
|
+
const { profileArguments } = await import('./spawn-adapters')
|
|
179
|
+
return {
|
|
180
|
+
profile,
|
|
181
|
+
args: [
|
|
182
|
+
...profileArguments(profile.runtime, profile),
|
|
183
|
+
...profile.args,
|
|
184
|
+
...extra,
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const collect = (value: string, previous: string[] = []) => [...previous, value]
|
|
189
|
+
export function profileCommands(agent: Command) {
|
|
190
|
+
for (const verb of ['add', 'edit'] as const) {
|
|
191
|
+
agent
|
|
192
|
+
.command(`${verb} <name>`)
|
|
193
|
+
.description(
|
|
194
|
+
`${verb} a local runtime profile (credentials are stored separately)`,
|
|
195
|
+
)
|
|
196
|
+
.option('--runtime <command>', 'native runtime or executable')
|
|
197
|
+
.option('--model <model>', 'native model ID; empty clears it')
|
|
198
|
+
.option('--effort <effort>', 'native reasoning effort; empty clears it')
|
|
199
|
+
.option(
|
|
200
|
+
'--dir <directory>',
|
|
201
|
+
'working directory, resolved to an absolute path',
|
|
202
|
+
)
|
|
203
|
+
.option(
|
|
204
|
+
'--arg <argument>',
|
|
205
|
+
'extra native argument; repeat to replace args',
|
|
206
|
+
collect,
|
|
207
|
+
)
|
|
208
|
+
.option(
|
|
209
|
+
'--env <name>',
|
|
210
|
+
'inherit only these environment variable names; repeat',
|
|
211
|
+
collect,
|
|
212
|
+
)
|
|
213
|
+
.option(
|
|
214
|
+
'--channel <owner/repo>',
|
|
215
|
+
'reserved channel mention subscription; repeat',
|
|
216
|
+
collect,
|
|
217
|
+
)
|
|
218
|
+
.option(
|
|
219
|
+
'--nudge <rule>',
|
|
220
|
+
'watchdog rule: idle:<duration>[:<limit>][:<message>], waiting:<duration>, ci:<owner/repo>, mention-unanswered:<duration>; repeat',
|
|
221
|
+
collect,
|
|
222
|
+
)
|
|
223
|
+
.option(
|
|
224
|
+
'--on <event.action[:label]>',
|
|
225
|
+
'wake this agent on a forge event, e.g. issues.labeled:triage; repeat',
|
|
226
|
+
collect,
|
|
227
|
+
)
|
|
228
|
+
.option('--clear-args', 'clear native arguments')
|
|
229
|
+
.option('--clear-env', 'restore normal environment inheritance')
|
|
230
|
+
.option('--clear-channels', 'clear reserved channels')
|
|
231
|
+
.option('--clear-nudges', 'clear watchdog rules')
|
|
232
|
+
.option('--clear-on', 'clear event triggers')
|
|
233
|
+
.option('--file <json>', 'read a complete, credential-free profile JSON')
|
|
234
|
+
.action(async (name: string, opts) => {
|
|
235
|
+
profilePath(name)
|
|
236
|
+
if (opts.file) {
|
|
237
|
+
if (Object.keys(opts).some((key) => key !== 'file'))
|
|
238
|
+
throw Error('--file cannot be combined with profile field options')
|
|
239
|
+
let value: unknown
|
|
240
|
+
try {
|
|
241
|
+
value = JSON.parse(await readFile(opts.file, 'utf8'))
|
|
242
|
+
} catch {
|
|
243
|
+
throw Error('Cannot read profile JSON')
|
|
244
|
+
}
|
|
245
|
+
const profile = parseProfile(value)
|
|
246
|
+
if (profile.name !== name)
|
|
247
|
+
throw Error('Agent profile name differs from the command name')
|
|
248
|
+
await saveProfile(profile, verb === 'add')
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
const previous =
|
|
252
|
+
verb === 'edit' ? await loadProfile(name) : { name, args: [] }
|
|
253
|
+
await saveProfile(
|
|
254
|
+
{
|
|
255
|
+
...previous,
|
|
256
|
+
...(opts.runtime !== undefined ? { runtime: opts.runtime } : {}),
|
|
257
|
+
...(opts.dir !== undefined ? { directory: resolve(opts.dir) } : {}),
|
|
258
|
+
...(opts.model !== undefined
|
|
259
|
+
? { model: opts.model || undefined }
|
|
260
|
+
: {}),
|
|
261
|
+
...(opts.effort !== undefined
|
|
262
|
+
? { effort: opts.effort || undefined }
|
|
263
|
+
: {}),
|
|
264
|
+
...(opts.arg
|
|
265
|
+
? { args: opts.arg }
|
|
266
|
+
: opts.clearArgs
|
|
267
|
+
? { args: [] }
|
|
268
|
+
: {}),
|
|
269
|
+
...(opts.env
|
|
270
|
+
? { env: opts.env }
|
|
271
|
+
: opts.clearEnv
|
|
272
|
+
? { env: undefined }
|
|
273
|
+
: {}),
|
|
274
|
+
...(opts.channel
|
|
275
|
+
? { channels: opts.channel }
|
|
276
|
+
: opts.clearChannels
|
|
277
|
+
? { channels: undefined }
|
|
278
|
+
: {}),
|
|
279
|
+
...(opts.nudge
|
|
280
|
+
? { nudges: opts.nudge }
|
|
281
|
+
: opts.clearNudges
|
|
282
|
+
? { nudges: undefined }
|
|
283
|
+
: {}),
|
|
284
|
+
...(opts.on
|
|
285
|
+
? { on: opts.on }
|
|
286
|
+
: opts.clearOn
|
|
287
|
+
? { on: undefined }
|
|
288
|
+
: {}),
|
|
289
|
+
},
|
|
290
|
+
verb === 'add',
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
agent
|
|
295
|
+
.command('rm <name>')
|
|
296
|
+
.description('remove a local runtime profile; keep approved credentials')
|
|
297
|
+
.action(async (name: string) => {
|
|
298
|
+
await loadProfile(name)
|
|
299
|
+
await unlink(profilePath(name))
|
|
300
|
+
})
|
|
301
|
+
agent
|
|
302
|
+
.command('ls')
|
|
303
|
+
.description('list local runtime profiles')
|
|
304
|
+
.option('--json', 'print profiles as JSON')
|
|
305
|
+
.action(async (opts) => {
|
|
306
|
+
await privateDirectory()
|
|
307
|
+
const profiles: AgentProfile[] = []
|
|
308
|
+
for (const entry of (await readdir(profilesDirectory())).sort()) {
|
|
309
|
+
if (entry.endsWith('.json'))
|
|
310
|
+
profiles.push(await loadProfile(entry.slice(0, -5)))
|
|
311
|
+
}
|
|
312
|
+
if (opts.json) console.log(JSON.stringify(profiles))
|
|
313
|
+
else {
|
|
314
|
+
console.log('NAME\tRUNTIME\tMODEL\tEFFORT\tDIRECTORY')
|
|
315
|
+
for (const p of profiles)
|
|
316
|
+
console.log(
|
|
317
|
+
[
|
|
318
|
+
p.name,
|
|
319
|
+
p.runtime,
|
|
320
|
+
p.model ?? '',
|
|
321
|
+
p.effort ?? '',
|
|
322
|
+
p.directory,
|
|
323
|
+
].join('\t'),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
// Identity establishment precedes a gf_ token. These legacy browser/terminal
|
|
3
|
+
// routes keep their key-proof protocol and are deliberately outside API v1.
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { ApiRequestError } from './client.ts'
|
|
6
|
+
const proof = z.object({
|
|
7
|
+
challenge: z.string(),
|
|
8
|
+
publicKey: z.string(),
|
|
9
|
+
signature: z.string(),
|
|
10
|
+
})
|
|
11
|
+
export const bootstrapRoutes = {
|
|
12
|
+
challenge: {
|
|
13
|
+
path: '/api/auth/challenge',
|
|
14
|
+
input: z.object({}),
|
|
15
|
+
output: z.object({ challenge: z.string() }),
|
|
16
|
+
},
|
|
17
|
+
answer: {
|
|
18
|
+
path: '/api/auth/answer',
|
|
19
|
+
input: proof,
|
|
20
|
+
output: z.object({ ok: z.boolean() }),
|
|
21
|
+
},
|
|
22
|
+
claim: {
|
|
23
|
+
path: '/api/claim/answer',
|
|
24
|
+
input: proof,
|
|
25
|
+
output: z.object({ ok: z.boolean(), name: z.string() }),
|
|
26
|
+
},
|
|
27
|
+
token: {
|
|
28
|
+
path: '/api/tokens',
|
|
29
|
+
input: proof,
|
|
30
|
+
output: z.object({ token: z.string(), name: z.string().nullable() }),
|
|
31
|
+
},
|
|
32
|
+
join: {
|
|
33
|
+
path: '/api/agents/requests',
|
|
34
|
+
input: z.object({
|
|
35
|
+
sponsor: z.string(),
|
|
36
|
+
label: z.string(),
|
|
37
|
+
publicKey: z.string(),
|
|
38
|
+
repo: z.string().optional(),
|
|
39
|
+
grants: z.array(z.string()),
|
|
40
|
+
note: z.string().optional(),
|
|
41
|
+
}),
|
|
42
|
+
output: z.object({
|
|
43
|
+
id: z.string(),
|
|
44
|
+
status: z.string(),
|
|
45
|
+
agent: z.string(),
|
|
46
|
+
approveUrl: z.string(),
|
|
47
|
+
collect: z.string(),
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
collect: {
|
|
51
|
+
path: '/api/agents/requests/{id}/token',
|
|
52
|
+
input: z.object({ signature: z.string() }),
|
|
53
|
+
output: z.object({
|
|
54
|
+
status: z.string(),
|
|
55
|
+
name: z.string().optional(),
|
|
56
|
+
token: z.string().optional(),
|
|
57
|
+
repo: z.string().nullable().optional(),
|
|
58
|
+
grants: z.array(z.string()).optional(),
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
} as const
|
|
62
|
+
export class ProofClient {
|
|
63
|
+
constructor(
|
|
64
|
+
readonly origin: string,
|
|
65
|
+
readonly fetcher: typeof fetch = fetch,
|
|
66
|
+
) {}
|
|
67
|
+
async request<I extends keyof typeof bootstrapRoutes>(
|
|
68
|
+
id: I,
|
|
69
|
+
input: z.input<(typeof bootstrapRoutes)[I]['input']>,
|
|
70
|
+
params: Record<string, string> = {},
|
|
71
|
+
) {
|
|
72
|
+
const route = bootstrapRoutes[id],
|
|
73
|
+
path = route.path.replace(/\{(\w+)\}/g, (_, name) =>
|
|
74
|
+
encodeURIComponent(params[name]),
|
|
75
|
+
)
|
|
76
|
+
const response = await this.fetcher(this.origin.replace(/\/$/, '') + path, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify(route.input.parse(input)),
|
|
80
|
+
redirect: 'error',
|
|
81
|
+
signal: AbortSignal.timeout(30000),
|
|
82
|
+
})
|
|
83
|
+
const data = await response
|
|
84
|
+
.json()
|
|
85
|
+
.catch(() => ({ error: `HTTP ${response.status}` }))
|
|
86
|
+
if (!response.ok)
|
|
87
|
+
throw new ApiRequestError(
|
|
88
|
+
response.status,
|
|
89
|
+
data.message ?? data.error ?? `HTTP ${response.status}`,
|
|
90
|
+
)
|
|
91
|
+
return route.output.parse(data) as z.output<
|
|
92
|
+
(typeof bootstrapRoutes)[I]['output']
|
|
93
|
+
>
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { channelState } from '../channel/state.ts'
|
|
4
|
+
export const channelCursor = z.string().regex(/^(0|[1-9][0-9]{0,14})$/)
|
|
5
|
+
export const channelAuthor = z.object({ name: z.string(), kind: z.enum(['human', 'agent']) })
|
|
6
|
+
export const channelMessage = z.object({
|
|
7
|
+
cursor: channelCursor, id: z.string(), created_at: z.string(),
|
|
8
|
+
reply_to: channelCursor.nullable(), author: channelAuthor, kind: z.enum(['message', 'system']), body: z.string(),
|
|
9
|
+
link: z.object({ href: z.string(), label: z.string() }).nullable(),
|
|
10
|
+
})
|
|
11
|
+
export const channelPage = z.object({ messages: z.array(channelMessage), cursor: channelCursor, before: channelCursor.nullable(), after: channelCursor.nullable() })
|
|
12
|
+
export const channelQuery = z.object({ before: channelCursor.optional(), after: channelCursor.optional(), limit: z.coerce.number().int().min(1).max(200).default(50) }).refine(q => !(q.before && q.after), 'Use before or after, not both')
|
|
13
|
+
export const channelInput = z.object({ body: z.string().trim().min(1).max(4000), reply_to: channelCursor.optional() })
|
|
14
|
+
export const channelParticipant = channelAuthor.extend({ prefix: z.enum(['@', '%', '+', '']), sponsor: z.string().nullable(), scopes: z.array(z.string()), online: z.boolean(), state: channelState.nullable().optional() })
|
|
15
|
+
export const channelParticipants = z.object({ participants: z.array(channelParticipant), can_post: z.boolean(), viewer: z.string().nullable() })
|
|
16
|
+
export type ChannelMessage = z.output<typeof channelMessage>
|
|
17
|
+
export type ChannelParticipant = z.output<typeof channelParticipant>
|
|
18
|
+
export type ChannelPage = z.output<typeof channelPage>
|
|
19
|
+
export type ChannelQuery = z.output<typeof channelQuery>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Generated by gild-site/scripts/generate-api-client.mjs. Edit the site source.
|
|
2
|
+
import {
|
|
3
|
+
routes,
|
|
4
|
+
type Operation,
|
|
5
|
+
type Input,
|
|
6
|
+
type Output,
|
|
7
|
+
type Query,
|
|
8
|
+
} from './contract.ts'
|
|
9
|
+
export class ApiRequestError extends Error {
|
|
10
|
+
readonly status: number
|
|
11
|
+
constructor(status: number, message: string) {
|
|
12
|
+
super(message)
|
|
13
|
+
this.status = status
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class GildClient {
|
|
17
|
+
constructor(
|
|
18
|
+
readonly baseURL: string,
|
|
19
|
+
readonly token: string,
|
|
20
|
+
readonly fetcher: (
|
|
21
|
+
input: RequestInfo | URL,
|
|
22
|
+
init?: RequestInit,
|
|
23
|
+
) => Promise<Response> = fetch,
|
|
24
|
+
) {}
|
|
25
|
+
async request<I extends Operation>(
|
|
26
|
+
operation: I,
|
|
27
|
+
params: Record<string, string | number> = {},
|
|
28
|
+
body?: Input<I>,
|
|
29
|
+
query?: Query<I>,
|
|
30
|
+
options?: { idempotencyKey?: string; signal?: AbortSignal },
|
|
31
|
+
): Promise<Output<I>> {
|
|
32
|
+
const route = routes.find((r) => r.id === operation)!
|
|
33
|
+
const path = route.path.replace(/\{(\w+)\}/g, (_, name: string) => {
|
|
34
|
+
if (params[name] === undefined)
|
|
35
|
+
throw new Error(`Missing path parameter: ${name}`)
|
|
36
|
+
const value = String(params[name])
|
|
37
|
+
return name === 'path' || name === 'ref'
|
|
38
|
+
? value.split('/').map(encodeURIComponent).join('/')
|
|
39
|
+
: encodeURIComponent(value)
|
|
40
|
+
})
|
|
41
|
+
const url = new URL(this.baseURL.replace(/\/$/, '') + path)
|
|
42
|
+
if (query)
|
|
43
|
+
for (const [name, value] of Object.entries(query))
|
|
44
|
+
if (value !== undefined) url.searchParams.set(name, String(value))
|
|
45
|
+
const res = await this.fetcher(url, {
|
|
46
|
+
method: route.method,
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${this.token}`,
|
|
49
|
+
'content-type': 'application/json',
|
|
50
|
+
...(options?.idempotencyKey
|
|
51
|
+
? { 'idempotency-key': options.idempotencyKey }
|
|
52
|
+
: {}),
|
|
53
|
+
},
|
|
54
|
+
body: ['GET', 'DELETE'].includes(route.method)
|
|
55
|
+
? undefined
|
|
56
|
+
: JSON.stringify(route.input.parse(body ?? {})),
|
|
57
|
+
signal: options?.signal ?? AbortSignal.timeout(30000),
|
|
58
|
+
redirect: 'error',
|
|
59
|
+
})
|
|
60
|
+
const data = res.status === 204 ? null : await res
|
|
61
|
+
.json()
|
|
62
|
+
.catch(() => ({ message: `HTTP ${res.status}` }))
|
|
63
|
+
if (!res.ok)
|
|
64
|
+
throw new ApiRequestError(
|
|
65
|
+
res.status,
|
|
66
|
+
data.message ?? `HTTP ${res.status}`,
|
|
67
|
+
)
|
|
68
|
+
return route.output.parse(data) as Output<I>
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Protocol adapters still resolve through canonical definitions.
|
|
72
|
+
export async function requestPath(
|
|
73
|
+
client: GildClient,
|
|
74
|
+
method: string,
|
|
75
|
+
path: string,
|
|
76
|
+
body?: unknown,
|
|
77
|
+
) {
|
|
78
|
+
for (const route of routes) {
|
|
79
|
+
if (route.method !== method) continue
|
|
80
|
+
const names = [...route.path.matchAll(/\{(\w+)\}/g)].map((m) => m[1]),
|
|
81
|
+
pattern = new RegExp(
|
|
82
|
+
'^' +
|
|
83
|
+
route.path
|
|
84
|
+
.split(/\{\w+\}/)
|
|
85
|
+
.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
86
|
+
.join('([^/]+)') +
|
|
87
|
+
'$',
|
|
88
|
+
),
|
|
89
|
+
matched = pattern.exec(path)
|
|
90
|
+
if (matched)
|
|
91
|
+
return client.request(
|
|
92
|
+
route.id,
|
|
93
|
+
Object.fromEntries(
|
|
94
|
+
names.map((n, i) => [n, decodeURIComponent(matched[i + 1])]),
|
|
95
|
+
),
|
|
96
|
+
body as Input<Operation>,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Unknown API operation: ${method} ${path}`)
|
|
100
|
+
}
|