@tangle-network/create-agent-app 0.47.3 → 0.47.4
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
|
@@ -45,11 +45,12 @@ Discovery: **Where does this app live and what may it spend?**
|
|
|
45
45
|
- [ ] `wrangler d1 create <name>` → paste `database_id` into `wrangler.toml`.
|
|
46
46
|
- [ ] Copy `.dev.vars.example` → `.dev.vars`; fill `BETTER_AUTH_SECRET`,
|
|
47
47
|
`TANGLE_API_KEY`, `SANDBOX_API_KEY`, `SANDBOX_GATEWAY_URL`.
|
|
48
|
-
- [ ] `pnpm db:migrate:local` — applies
|
|
49
|
-
buffer, API keys, usage, and
|
|
48
|
+
- [ ] `pnpm db:migrate:local` — applies migrations for auth, chat, turn
|
|
49
|
+
buffer, API keys, usage, request limits, and spending reservations.
|
|
50
50
|
The e2e test executes these same files, so they cannot drift from the schema.
|
|
51
51
|
- [ ] Existing generated app: copy and apply `0002_agent_gateway.sql`.
|
|
52
52
|
Never edit its already-applied `0001_init.sql`.
|
|
53
|
+
- [ ] Apply `0003_gateway_reservations.sql` when adopting the gateway reservation lifecycle.
|
|
53
54
|
- [ ] Existing app only: add `sandbox_prewarm_claims` in a new migration.
|
|
54
55
|
Do not edit an applied `0001_init.sql`; Wrangler will not run it again.
|
|
55
56
|
- [ ] R2 stays commented out unless the product stores artifacts.
|
|
@@ -64,6 +65,9 @@ Discovery: **Does a real message round-trip through a real box?**
|
|
|
64
65
|
- [ ] Kill the tab mid-turn, reopen the thread — the persisted row is intact
|
|
65
66
|
(the turn keeps running server-side and buffers for replay).
|
|
66
67
|
- [ ] Create a key through the signed-in `/api/keys` route.
|
|
68
|
+
Finite-cap keys require backend enforcement of per-turn spending limits.
|
|
69
|
+
The current remote chat adapter rejects capped execution before compute.
|
|
70
|
+
Explicitly uncapped keys use the existing chat path.
|
|
67
71
|
- [ ] Call `/v1/agents/<slug>/chat/completions` with that key.
|
|
68
72
|
Open the returned `X-Tangle-Thread-Url` and confirm it shows the same durable conversation.
|
|
69
73
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@tangle-network/agent-app": "__AGENT_APP_VERSION__",
|
|
23
|
-
"@tangle-network/agent-gateway": "0.
|
|
23
|
+
"@tangle-network/agent-gateway": "0.10.0",
|
|
24
24
|
"@tangle-network/agent-interface": "2.3.0",
|
|
25
25
|
"@tangle-network/agent-runtime": "0.192.2",
|
|
26
26
|
"@tangle-network/sandbox": "0.37.0",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS agent_api_key_reservation (
|
|
2
|
+
request_id TEXT PRIMARY KEY,
|
|
3
|
+
key_id TEXT NOT NULL,
|
|
4
|
+
reserved_cents BIGINT NOT NULL,
|
|
5
|
+
state TEXT NOT NULL,
|
|
6
|
+
created_at BIGINT NOT NULL,
|
|
7
|
+
FOREIGN KEY (key_id) REFERENCES agent_api_key(id) ON DELETE CASCADE
|
|
8
|
+
);
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_agent_api_key_reservation_key
|
|
10
|
+
ON agent_api_key_reservation (key_id);
|
|
@@ -97,6 +97,7 @@ export function buildGatewayApp(
|
|
|
97
97
|
: null,
|
|
98
98
|
verifyApiKey: verifyKey,
|
|
99
99
|
claimApiKeyRequest: createApiKeyRequestClaim(apiKeys),
|
|
100
|
+
apiKeyReservationLifecycle: apiKeys.reservations,
|
|
100
101
|
apiKeyPrefix: 'ak_',
|
|
101
102
|
conversationMode: 'thread',
|
|
102
103
|
// A2A task control remains off until the shared gateway owns durable
|
|
@@ -61,6 +61,7 @@ const MIGRATIONS = readdirSync(MIGRATIONS_DIR)
|
|
|
61
61
|
.map((name) => join(MIGRATIONS_DIR, name))
|
|
62
62
|
const BASE_MIGRATION = join(MIGRATIONS_DIR, '0001_init.sql')
|
|
63
63
|
const GATEWAY_MIGRATION = join(MIGRATIONS_DIR, '0002_agent_gateway.sql')
|
|
64
|
+
const RESERVATION_MIGRATION = join(MIGRATIONS_DIR, '0003_gateway_reservations.sql')
|
|
64
65
|
|
|
65
66
|
/** The real migration, executed against a real SQLite database. Every query
|
|
66
67
|
* the test makes afterwards runs over THESE tables — schema drift between
|
|
@@ -412,7 +413,8 @@ describe('e2e: fake sandbox producer → streamed turn → persisted transcript'
|
|
|
412
413
|
})
|
|
413
414
|
|
|
414
415
|
it('the migration carries every agent-gateway SQL store statement', () => {
|
|
415
|
-
const migration =
|
|
416
|
+
const migration = [GATEWAY_MIGRATION, RESERVATION_MIGRATION]
|
|
417
|
+
.map((path) => readFileSync(path, 'utf8')).join('\n')
|
|
416
418
|
const normalize = (sql: string) => sql.replace(/\s+/g, ' ').replace(/;$/, '').trim()
|
|
417
419
|
const statements = [
|
|
418
420
|
...sqlApiKeyStoreSchemaStatements(),
|
|
@@ -440,6 +442,30 @@ describe('e2e: fake sandbox producer → streamed turn → persisted transcript'
|
|
|
440
442
|
])
|
|
441
443
|
})
|
|
442
444
|
|
|
445
|
+
it('rejects capped remote execution before starting a chat turn', async () => {
|
|
446
|
+
let starts = 0
|
|
447
|
+
const { workerFetch, sql, cookie } = await createHarness(() => {
|
|
448
|
+
starts += 1
|
|
449
|
+
return createSandboxChatProducer({ events: feed(RAW_TURN_EVENTS), model: MODEL })
|
|
450
|
+
})
|
|
451
|
+
const keyResponse = await workerFetch(post('/api/keys', cookie, {
|
|
452
|
+
name: 'bounded caller', spendingLimitCents: 100_000,
|
|
453
|
+
}))
|
|
454
|
+
expect(keyResponse.status).toBe(201)
|
|
455
|
+
const { key } = (await keyResponse.json()) as { key: string }
|
|
456
|
+
const response = await workerFetch(new Request(`${BASE}/v1/agents/${appSlug}/chat/completions`, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
459
|
+
body: JSON.stringify({ messages: [{ role: 'user', content: 'Read my lease' }], stream: true }),
|
|
460
|
+
}))
|
|
461
|
+
expect(response.status, await response.clone().text()).toBe(200)
|
|
462
|
+
expect(await response.text()).toContain('api_key.execution_budget_unsupported')
|
|
463
|
+
expect(starts).toBe(0)
|
|
464
|
+
expect(await sql.query('SELECT state FROM agent_api_key_reservation'))
|
|
465
|
+
.toEqual([{ state: 'released' }])
|
|
466
|
+
expect(await sql.query('SELECT cost_cents FROM agent_api_key_usage')).toEqual([])
|
|
467
|
+
})
|
|
468
|
+
|
|
443
469
|
it('shares one owned thread across OpenAI-compatible API calls', async () => {
|
|
444
470
|
const { app, workerFetch, sql, cookie, gatewayBuilds, settle } = await createHarness()
|
|
445
471
|
const cardResponse = await workerFetch(new Request(
|