Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Corollary — **extend, never duplicate.** Before writing anything that complete
| `/chat-routes` | the assembled server chat vertical (#188 Phase 1): `createChatTurnRoutes()` — body parse/validate → injected `authorize` seam → producer seam → turn-buffer tap wired BY DEFAULT → NDJSON `Response` + replay endpoint + composed `/interactions` answer endpoints + `/chat-store` persistence (user row on send, assistant row with parts/usage on completion); `createSandboxChatProducer` (raw sandbox events → client vocabulary + persisted parts + usage receipt, non-renderable-ask auto-decline); `createUploadRoute` (multipart → inline `data:` part ≤700 KiB, else base64 write through a structural sandbox sink + path-ref part — the >1 MiB gateway cap makes the two-step mandatory); import-free `./wire` contract (`ChatTurnRequestPayload`, inline-part byte gate) re-exported via `/web-react`'s chat-stream | peer `@tangle-network/agent-runtime` (`handleChatTurn` IS the turn engine — zero loop logic here; subpath-only, not in the root barrel); composes `/stream`, `/chat-store`, `/interactions`, `/web`. Reference assembly: `examples/chat-app.md` |
| `/tangle` | app-registration consent URL + cached broker-token provider | structural `TangleAppsClient` (from agent-integrations) |
| `/billing` | per-workspace budget-capped key manager (mint/rotate/rollover/usage) | structural tcloud provisioner + store + crypto seams |
| `/preflight` | deploy-time secret-liveness probes: `runPreflight(probes)` → per-probe verdict + latency + overall pass/fail (any critical fail → fail); standard builders `routerChatProbe`/`sandboxAuthProbe`/`httpHeadProbe` (explicit config, read nothing global); `formatPreflightReport` table + the `agent-app-preflight` bin reading `preflight.config.mjs`. Binds at DEPLOY time (the one place with real secrets — CI can't hold them); failures name the exact secret to rotate | — (server-only; product declares probes from `process.env`) |
| `/missions` | durable multi-step mission orchestration: guarded status/step machine + cursor + cost ledger over a `MissionStorePort` (CAS updates → typed conflict, opaque `extras` insert passthrough for product columns), idempotent plan engine (cached-done short-circuit, cursor reconciliation, retryable-vs-deterministic failure, detached-session polling), budget/classification/volume gates that park as `waiting_approval`, `:::mission` parser, client-safe live-event reducer + the canonical `StepAgentActivity` per-step delegated-run lane (`step.updated` snapshot, latest-wins) | — (substrate-free; product supplies storage, `SandboxDispatch`, approvals port, `classifyStep`) |
| `/trace` | flow observability: FlowSpan/FlowTrace + ASCII waterfall/histogram renderers; mission trace bridge (`createMissionTraceContext`/`childSpanContext`/`traceEnv` — 32-hex/16-hex ids + the `TRACE_ID`/`PARENT_SPAN_ID` env pair agent-runtime's `readTraceContextFromEnv` inherits); delegation→FlowSpan converters (`delegationActivityToFlowSpans`, `loopTraceEventsToFlowSpans` over a structural `LoopTraceEventLike`, `composeMissionFlowTrace`, `stepActivityFlowTrace`) | — (pure data; id formats byte-match agent-runtime's OTLP export, no import) |
| `/web-react` | shared chat-shell + observability components: ModelPicker/EffortPicker/ChatMessages/RunDrillIn, `MissionActivityLane` (per-step delegated-run sub-rows → web waterfall), `AgentActivityPanel` (cross-context delegation surface over a `fetchActivity(cursor)` data port, missionRef link slot), `FlowWaterfall` + pure `waterfallLayout`/`mergeActivityPages` helpers, `InteractionQuestionCard`/`InteractionPlanCard` + `useChatInteractions`/`createInteractionAnswerSubmitter` (the client half of `/interactions`) | react peer; renders `/missions` lanes via `/trace` converters and `/interactions` asks via its contract |
Expand Down
47 changes: 47 additions & 0 deletions bin/preflight.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env node
/**
* `agent-app-preflight` — run a product's declared secret-liveness probes as a
* DEPLOY step and fail the deploy if any critical secret is dead.
*
* Reads `preflight.config.mjs` from the current working directory (override
* with `PREFLIGHT_CONFIG`). That file default-exports the probes, built from
* `process.env` at load time — the deploy already has the real secrets in its
* environment, which is the one place they can be probed for liveness (CI
* cannot hold them). Exit 0 when every critical probe is live, 1 when one is
* dead (deploy fails), 2 on a config/usage error.
*
* Wire it as a step BEFORE `wrangler deploy`:
* agent-app-preflight
*/
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'

import { formatPreflightReport, runPreflight } from '../dist/preflight/index.js'

const configFile = process.env.PREFLIGHT_CONFIG ?? 'preflight.config.mjs'

async function loadProbes() {
const path = resolve(process.cwd(), configFile)
if (!existsSync(path)) {
console.error(`preflight: no config at ${path}`)
console.error(
`Create ${configFile} default-exporting probes built from process.env — see '@tangle-network/agent-app/preflight'.`,
)
process.exit(2)
}
const mod = await import(pathToFileURL(path).href)
const exported = mod.default ?? mod.probes
const resolved = typeof exported === 'function' ? await exported() : exported
const probes = Array.isArray(resolved) ? resolved : resolved?.probes
if (!Array.isArray(probes) || probes.length === 0) {
console.error(`preflight: ${configFile} must default-export a non-empty array of probes (or { probes }).`)
process.exit(2)
}
return probes
}

const probes = await loadProbes()
const report = await runPreflight(probes)
console.log(formatPreflightReport(report))
process.exit(report.ok ? 0 : 1)
55 changes: 55 additions & 0 deletions examples/preflight.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Example `preflight.config.mjs` — legal-agent's four secret-liveness probes.
*
* Copy this to a product's repo root. The deploy workflow runs
* `agent-app-preflight` (added by this package's `bin`) as a step before
* `wrangler deploy`; a dead secret fails the deploy with a message naming
* exactly which secret to rotate.
*
* Each probe below guards a secret from the 2026-07-15 incident — four secrets
* dead in one production day, all present in `wrangler secret list`, none
* checked for liveness anywhere:
* 1. routerChatProbe → LITELLM_API_KEY (dead key) + LITELLM_BASE_URL (dead url)
* 2. sandboxAuthProbe → SANDBOX_API_KEY (dead key) + SANDBOX_API_URL (stale url)
* 3. httpHeadProbe → TANGLE_PLATFORM_URL reachability (stale platform url)
* 4. httpHeadProbe → session-gateway reachability (non-critical: warn only)
*
* The config reads nothing but `process.env`, so the same file runs in the
* deploy environment, a smoke test, or a local check.
*/
import { httpHeadProbe, routerChatProbe, sandboxAuthProbe } from '@tangle-network/agent-app/preflight'

const env = process.env

const trimSlash = (url) => (url ?? '').replace(/\/+$/, '')

export default [
routerChatProbe({
name: 'router-chat',
baseUrl: env.LITELLM_BASE_URL,
apiKey: env.LITELLM_API_KEY,
model: env.PREFLIGHT_ROUTER_MODEL ?? 'gpt-4o-mini',
keySecret: 'LITELLM_API_KEY',
urlSecret: 'LITELLM_BASE_URL',
}),
sandboxAuthProbe({
name: 'sandbox-auth',
baseUrl: env.SANDBOX_API_URL,
apiKey: env.SANDBOX_API_KEY,
keySecret: 'SANDBOX_API_KEY',
urlSecret: 'SANDBOX_API_URL',
}),
httpHeadProbe({
name: 'tangle-platform',
url: `${trimSlash(env.TANGLE_PLATFORM_URL)}/health`,
urlSecret: 'TANGLE_PLATFORM_URL',
}),
httpHeadProbe({
name: 'session-gateway',
url: `${trimSlash(env.SESSION_GATEWAY_URL)}/health`,
urlSecret: 'SESSION_GATEWAY_URL',
// A degraded gateway should warn the operator, not block a deploy that fixes
// other secrets — the interactive stream reconnects when it recovers.
critical: false,
}),
]
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@
"types": "./dist/index.d.ts",
"files": [
"dist",
"bin",
".claude/skills"
],
"bin": {
"agent-app-preflight": "./bin/preflight.mjs"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -138,6 +142,11 @@
"import": "./dist/billing/index.js",
"default": "./dist/billing/index.js"
},
"./preflight": {
"types": "./dist/preflight/index.d.ts",
"import": "./dist/preflight/index.js",
"default": "./dist/preflight/index.js"
},
"./chat-store": {
"types": "./dist/chat-store/index.d.ts",
"import": "./dist/chat-store/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './harness/index'
export * from './config/index'
export * from './preset-cloudflare/index'
export * from './billing/index'
export * from './preflight/index'
// `/chat-store`'s drizzle factory + store stay subpath-only (they import the
// optional drizzle-orm peer at module top); its pure pieces — the stored
// `parts` vocabulary, title derivation, input error — are safe here.
Expand Down
Loading