@tangle-network/create-agent-app 0.46.60 → 0.47.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/create-agent-app",
3
- "version": "0.46.60",
3
+ "version": "0.47.2",
4
4
  "description": "Create a tested Tangle agent application with either a tool loop or a complete chat interface.",
5
5
  "keywords": [
6
6
  "tangle",
@@ -51,3 +51,11 @@ Then walk the trail:
51
51
  Identity comes from the session, never a request body. Inaccessible threads
52
52
  read as 404. No mock agent — missing sandbox credentials fail loud. See
53
53
  `AGENTS.md` for the full contract.
54
+
55
+ ## Artifact index
56
+
57
+ `GET /api/files` lists files under `/home/agent/artifacts` for the signed-in user's workspace.
58
+ The default prompt directs the agent to save user-facing outputs there; other workspace files are not indexed.
59
+ The route returns `ready` with filtered metadata or `warming` when no ready sandbox exists.
60
+ It never provisions or resumes a sandbox, and it does not provide file downloads.
61
+ Hidden files and paths outside the artifact root are excluded.
@@ -9,3 +9,6 @@ Hard rules:
9
9
  - Never fabricate a figure (price, identifier, clause, date). Cite a real record or say NOT ON FILE.
10
10
  - Route every regulated or client-facing action to a named human for approval; propose, don't execute.
11
11
  - State what you did and what evidence backs it. No filler.
12
+
13
+ Save files created for the user under `/home/agent/artifacts` so they appear in the workspace artifact index.
14
+ Keep credentials, configuration, and private working files outside that directory.
@@ -0,0 +1,30 @@
1
+ import { createSandboxFileIndexRoute } from '@tangle-network/agent-app/chat-routes'
2
+ import { peekWorkspaceSandbox } from '@tangle-network/agent-app/sandbox'
3
+ import type { ChatApp } from './chat'
4
+ import type { AppEnv } from './env'
5
+ import { createSandboxShell } from './sandbox'
6
+
7
+ /** Agent output directory; configuration and other workspace files are excluded. */
8
+ export const ARTIFACT_ROOT = '/home/agent/artifacts'
9
+
10
+ export function createArtifactIndex(
11
+ env: AppEnv,
12
+ app: ChatApp,
13
+ peek: typeof peekWorkspaceSandbox = peekWorkspaceSandbox,
14
+ ) {
15
+ return createSandboxFileIndexRoute({
16
+ authorize: async ({ request }) => {
17
+ const session = await app.auth.getSession(request)
18
+ if (!session) return { status: 'denied', response: Response.json({ error: 'Unauthorized' }, { status: 401 }) }
19
+ const userId = session.user.id
20
+ const requestedWorkspace = new URL(request.url).searchParams.get('workspaceId')
21
+ if (requestedWorkspace !== null && requestedWorkspace !== userId) {
22
+ return { status: 'denied', response: Response.json({ error: 'Not found' }, { status: 404 }) }
23
+ }
24
+ const found = await peek(createSandboxShell(env), { workspaceId: userId, userId })
25
+ if (found.status !== 'running') return { status: 'warming' }
26
+ return { status: 'ready', fs: found.box.fs, root: ARTIFACT_ROOT }
27
+ },
28
+ maxEntries: 1000,
29
+ })
30
+ }
@@ -11,6 +11,7 @@
11
11
  * POST /api/chat run one turn (NDJSON stream)
12
12
  * GET /api/chat/replay/:turnId replay a buffered turn (?fromSeq=)
13
13
  * GET /api/chat/running live turn ids on a thread (?threadId=)
14
+ * GET /api/files owned artifact index (no provisioning)
14
15
  * POST /api/chat/upload multipart upload → prompt parts
15
16
  * GET /api/chat/interactions outstanding agent asks (?threadId=)
16
17
  * POST /api/chat/interactions answer an ask
@@ -18,6 +19,8 @@
18
19
  * POST /v1/agents/:slug/chat/completions OpenAI-compatible API
19
20
  */
20
21
 
22
+ import { createArtifactIndex } from './files'
23
+ import type { peekWorkspaceSandbox } from '@tangle-network/agent-app/sandbox'
21
24
  import { config } from '../agent.config'
22
25
  import { buildChatApp, type ChatApp } from './chat'
23
26
  import type { AppEnv } from './env'
@@ -35,6 +38,8 @@ export interface WorkerAssembly {
35
38
  ): ReturnType<typeof buildGatewayApp>
36
39
  /** Test override. Production follows agent.config.ts. */
37
40
  gatewayEnabled?: boolean
41
+ /** Test seam; production reads existing sandbox state without provisioning. */
42
+ peekWorkspace?: typeof peekWorkspaceSandbox
38
43
  }
39
44
 
40
45
  function attachThreadUrl(response: Response, request: Request): Response {
@@ -100,6 +105,10 @@ export function createWorker(assembly: WorkerAssembly = defaultAssembly): Export
100
105
  return attachThreadUrl(await gateway.fetch(request), request)
101
106
  }
102
107
 
108
+ if (pathname === '/api/files' && method === 'GET') {
109
+ return createArtifactIndex(env, app, assembly.peekWorkspace)(request)
110
+ }
111
+
103
112
  if (pathname.startsWith('/api/auth/')) return app.auth.auth.handler(request)
104
113
 
105
114
  if (pathname === '/api/chat' && method === 'POST') {
@@ -21,7 +21,7 @@ import { dirname, join } from 'node:path'
21
21
  import { fileURLToPath } from 'node:url'
22
22
  import Database from 'better-sqlite3'
23
23
  import { drizzle } from 'drizzle-orm/better-sqlite3'
24
- import { describe, expect, it } from 'vitest'
24
+ import { describe, expect, it, vi } from 'vitest'
25
25
 
26
26
  import {
27
27
  createSandboxChatProducer,
@@ -46,7 +46,8 @@ import { buildChatApp, type ChatApp } from '../src/chat'
46
46
  import type { AppEnv } from '../src/env'
47
47
  import { buildGatewayApp } from '../src/gateway'
48
48
  import { appSlug } from '../src/sandbox'
49
- import { createWorker } from '../src/worker'
49
+ import { ARTIFACT_ROOT } from '../src/files'
50
+ import { createWorker, type WorkerAssembly } from '../src/worker'
50
51
 
51
52
  const BASE = 'http://localhost:8787'
52
53
  const MODEL = 'test/model-1'
@@ -126,6 +127,7 @@ interface Harness {
126
127
  async function createHarness(
127
128
  produce: (args: ChatTurnProduceArgs<void>) => ChatTurnRouteProducer = () =>
128
129
  createSandboxChatProducer({ events: feed(RAW_TURN_EVENTS), model: MODEL }),
130
+ peekWorkspace?: WorkerAssembly['peekWorkspace'],
129
131
  ): Promise<Harness> {
130
132
  const database = openMigratedDb()
131
133
  const pending: Promise<unknown>[] = []
@@ -159,6 +161,7 @@ async function createHarness(
159
161
  app.routes.turn = (request) =>
160
162
  originalTurn(request, { waitUntil: (p) => void pending.push(p) })
161
163
  const worker = createWorker({
164
+ peekWorkspace,
162
165
  buildChatApp: () => app,
163
166
  buildGatewayApp: (_env, chatApp, options) => {
164
167
  gatewayBuildCount += 1
@@ -234,6 +237,42 @@ async function readGatewayText(response: Response): Promise<string> {
234
237
  // ── the gate ────────────────────────────────────────────────────────────────
235
238
 
236
239
  describe('e2e: fake sandbox producer → streamed turn → persisted transcript', () => {
240
+ it('indexes only owned artifacts through the real authenticated Worker route', async () => {
241
+ expect(config.systemPrompt).toContain(ARTIFACT_ROOT)
242
+ const tree = vi.fn(async () => ({
243
+ root: '/home/agent/artifacts',
244
+ files: [
245
+ { path: '/home/agent/artifacts/report.txt', size: 7 },
246
+ { path: '/home/agent/artifacts/.env', size: 99 },
247
+ { path: '/home/agent/private.txt', size: 99 },
248
+ { path: '../private.txt', size: 99 },
249
+ ],
250
+ stats: { truncated: false },
251
+ }))
252
+ const peek = vi.fn().mockResolvedValue({ status: 'running', box: { fs: { tree } } })
253
+ const h = await createHarness(undefined, peek)
254
+ expect((await h.workerFetch(new Request(`${BASE}/api/files`))).status).toBe(401)
255
+ expect(peek).not.toHaveBeenCalled()
256
+ const denied = await h.workerFetch(new Request(`${BASE}/api/files?workspaceId=another-user`, {
257
+ headers: { cookie: h.cookie },
258
+ }))
259
+ expect(denied.status).toBe(404)
260
+ expect(peek).not.toHaveBeenCalled()
261
+ const response = await h.workerFetch(new Request(`${BASE}/api/files?root=/home/agent`, {
262
+ headers: { cookie: h.cookie },
263
+ }))
264
+ expect(response.status).toBe(200)
265
+ expect(await response.json()).toMatchObject({ status: 'ready', files: [{ path: 'report.txt', name: 'report.txt', size: 7 }] })
266
+ expect(tree).toHaveBeenCalledWith('/home/agent/artifacts', { maxDepth: 12 })
267
+ const session = await h.app.auth.getSession(new Request(BASE, { headers: { cookie: h.cookie } }))
268
+ expect(peek).toHaveBeenCalledWith(expect.anything(), { userId: session!.user.id, workspaceId: session!.user.id })
269
+ peek.mockResolvedValue({ status: 'absent' })
270
+ tree.mockClear()
271
+ const cold = await h.workerFetch(new Request(`${BASE}/api/files`, { headers: { cookie: h.cookie } }))
272
+ expect(await cold.json()).toEqual({ status: 'warming' })
273
+ expect(tree).not.toHaveBeenCalled()
274
+ })
275
+
237
276
  it('normalizes path-backed generic files for the sandbox prompt API', () => {
238
277
  expect(
239
278
  normalizeChatPromptForSandbox([