@ai-ecoverse/slicc-agent 3.43.0 → 3.44.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.
Files changed (67) hide show
  1. package/README.md +15 -5
  2. package/dist/agent.js +1 -0
  3. package/dist/agent.js.map +2 -2
  4. package/dist/browser/guard.js +19 -24
  5. package/dist/browser/guard.js.map +2 -2
  6. package/dist/browser/origins.js +7 -37
  7. package/dist/browser/origins.js.map +2 -2
  8. package/dist/client.js +3 -1
  9. package/dist/client.js.map +2 -2
  10. package/dist/codemode/index.js +2 -1
  11. package/dist/codemode/index.js.map +2 -2
  12. package/dist/control.js +13 -3
  13. package/dist/control.js.map +2 -2
  14. package/dist/freezer/index.js +5 -4
  15. package/dist/freezer/index.js.map +2 -2
  16. package/dist/host.js +13 -2
  17. package/dist/host.js.map +2 -2
  18. package/dist/kernel/env.js +55 -4
  19. package/dist/kernel/env.js.map +2 -2
  20. package/dist/kernel/exec.js +84 -16
  21. package/dist/kernel/exec.js.map +3 -3
  22. package/dist/licks/licks.js +17 -1
  23. package/dist/licks/licks.js.map +2 -2
  24. package/dist/main.js +29 -10
  25. package/dist/main.js.map +2 -2
  26. package/dist/mcp/content.js +3 -1
  27. package/dist/mcp/content.js.map +2 -2
  28. package/dist/mcp/tools.js +2 -1
  29. package/dist/mcp/tools.js.map +2 -2
  30. package/dist/memory/prompt.js +3 -2
  31. package/dist/memory/prompt.js.map +2 -2
  32. package/dist/memory/store.js +5 -3
  33. package/dist/memory/store.js.map +2 -2
  34. package/dist/remote/host.js +21 -1
  35. package/dist/remote/host.js.map +2 -2
  36. package/dist/secrets/extension.js +150 -0
  37. package/dist/secrets/extension.js.map +7 -0
  38. package/dist/secrets/names.js +72 -0
  39. package/dist/secrets/names.js.map +7 -0
  40. package/dist/secrets/scrub.js +36 -0
  41. package/dist/secrets/scrub.js.map +7 -0
  42. package/dist/secrets/vault.js +161 -0
  43. package/dist/secrets/vault.js.map +7 -0
  44. package/dist/services.js.map +2 -2
  45. package/dist/skills/index.js +8 -1
  46. package/dist/skills/index.js.map +2 -2
  47. package/dist/spectrum/agent-port.js +6 -2
  48. package/dist/spectrum/agent-port.js.map +2 -2
  49. package/dist/types/agent.d.ts +1 -0
  50. package/dist/types/browser/guard.d.ts +5 -3
  51. package/dist/types/browser/origins.d.ts +19 -4
  52. package/dist/types/client.d.ts +2 -0
  53. package/dist/types/control.d.ts +2 -1
  54. package/dist/types/host.d.ts +9 -1
  55. package/dist/types/kernel/env.d.ts +6 -0
  56. package/dist/types/kernel/exec.d.ts +8 -1
  57. package/dist/types/licks/licks.d.ts +1 -0
  58. package/dist/types/mcp/content.d.ts +1 -1
  59. package/dist/types/remote/host.d.ts +5 -1
  60. package/dist/types/secrets/extension.d.ts +31 -0
  61. package/dist/types/secrets/names.d.ts +24 -0
  62. package/dist/types/secrets/scrub.d.ts +21 -0
  63. package/dist/types/secrets/vault.d.ts +30 -0
  64. package/dist/types/services.d.ts +3 -1
  65. package/dist/types/skills/index.d.ts +1 -0
  66. package/dist/types/spectrum/agent-port.d.ts +2 -0
  67. package/package.json +4 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/remote/host.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT, withCancel } from '@earendil-works/chord/context';\nimport {\n type AgentEvent,\n type AgentEventStream,\n type Conversation,\n type EntryRecord,\n type Harness,\n type SnapshotEvent,\n type SubmissionId,\n watchEvents,\n} from '@earendil-works/pi-durable';\nimport type { Delivered, SendMode, SendRequest, SendResponse } from '../services.ts';\nimport { type EndReason, idOf, Mapper, type RemoteEvent, replayOf } from './events.ts';\n\nexport const MAX_QUEUED = 10000;\nexport const IDLE_MS = 120000;\n\nexport type RemoteOpened =\n | { ok: true; stream: string; delivered: Delivered | null; head: string | null; gap: boolean }\n | { ok: false; code: string; message: string };\n\nexport interface RemoteBatch {\n events: RemoteEvent[];\n done: boolean;\n}\n\nexport interface AgentRemote {\n prompt(\n request: { agentId: string | null; text: string; whenBusy: SendMode; thinking: boolean },\n context: Context\n ): Promise<RemoteOpened>;\n watch(\n request: { agentId: string | null; since: string | null; thinking: boolean },\n context: Context\n ): Promise<RemoteOpened>;\n next(stream: string, waitMs: number, context: Context): Promise<RemoteBatch>;\n close(stream: string, context: Context): Promise<void>;\n abort(agentId: string | null, context: Context): Promise<{ aborted: boolean }>;\n}\n\nexport interface RemoteHost {\n harness: Harness;\n conversation(agentId: string | null, context: Context): Promise<Conversation>;\n send(request: SendRequest, context: Context): Promise<SendResponse>;\n now?: () => number;\n limit?: number;\n sweepMs?: number;\n}\n\nexport class Stream<T = RemoteEvent> {\n readonly queue: T[] = [];\n done = false;\n touched: number;\n #wake: (() => void) | undefined;\n readonly #stop: () => Promise<unknown>;\n readonly #limit: number;\n readonly #overflow: readonly T[];\n\n constructor(stop: () => Promise<unknown>, now: number, limit: number, overflow: readonly T[]) {\n this.#stop = stop;\n this.touched = now;\n this.#limit = limit;\n this.#overflow = overflow;\n }\n\n push(events: readonly T[]): void {\n if (this.done || !events.length) return;\n this.queue.push(...events);\n if (this.queue.length > this.#limit) {\n this.queue.length = 0;\n this.queue.push(...this.#overflow);\n void this.finish();\n }\n this.#wake?.();\n }\n\n async finish(): Promise<void> {\n if (this.done) return;\n this.done = true;\n this.#wake?.();\n await this.#stop().catch(() => undefined);\n }\n\n wait(ms: number, signal?: AbortSignal): Promise<void> {\n if (this.queue.length || this.done) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(done, ms);\n signal?.addEventListener('abort', done, { once: true });\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n this.#wake = () => {\n this.#wake = undefined;\n done();\n };\n });\n }\n}\n\nfunction deliveredOf(\n status: { status: string } | undefined,\n whenBusy: SendMode,\n busy: boolean\n): Delivered {\n if (status?.status === 'queued') return whenBusy === 'followUp' ? 'follow-up' : 'steer';\n return busy ? 'steer' : 'run';\n}\n\nfunction headOf(events: readonly RemoteEvent[]): string | null {\n for (let at = events.length - 1; at >= 0; at--) {\n const id = idOf(events[at] as RemoteEvent);\n if (id) return id;\n }\n return null;\n}\n\nexport function followed(mapper: Mapper, event: AgentEvent): RemoteEvent[] {\n return event.type === 'snapshot' ? resync(mapper, event) : mapper.event(event);\n}\n\nexport function resync(mapper: Mapper, snapshot: SnapshotEvent): RemoteEvent[] {\n const last = mapper.last();\n return [\n ...snapshot.entries.filter((entry) => entry.id > last).flatMap((entry) => mapper.entry(entry)),\n ...mapper.partial(snapshot.generation?.message as never),\n ];\n}\n\nfunction placedEntry(value: unknown): number | undefined {\n const entry = (value as { entry?: unknown } | undefined)?.entry;\n return typeof entry === 'number' ? entry : undefined;\n}\n\nexport class Follower {\n readonly #mapper: Mapper;\n readonly #entries: Map<number, EntryRecord>;\n readonly #submission: SubmissionId;\n readonly #delivered: Delivered;\n readonly #out: RemoteEvent[] = [];\n #user: number | undefined;\n #started = false;\n\n constructor(\n mapper: Mapper,\n entries: readonly EntryRecord[],\n submission: SubmissionId,\n delivered: Delivered,\n record: unknown\n ) {\n this.#mapper = mapper;\n this.#entries = new Map(entries.map((entry) => [entry.id, entry]));\n this.#submission = submission;\n this.#delivered = delivered;\n this.#user = placedEntry(record);\n }\n\n drain(): RemoteEvent[] {\n return this.#out.splice(0);\n }\n\n #begin(): void {\n const entry = this.#user === undefined ? undefined : this.#entries.get(this.#user);\n if (!entry) return;\n this.#started = true;\n this.#out.push(...this.#mapper.entry(entry, this.#delivered));\n }\n\n #end(reason: EndReason, message?: string): true {\n this.#out.push({ t: 'end', reason, ...(message ? { message } : {}) });\n return true;\n }\n\n #ours(event: AgentEvent): boolean {\n if (event.type !== 'submission' || event.record.id !== this.#submission) return false;\n this.#user = placedEntry(event.record) ?? this.#user;\n return this.#user === undefined && !['queued', 'placed'].includes(event.record.status);\n }\n\n take(batch: readonly AgentEvent[]): boolean {\n for (const event of batch) {\n if (event.type === 'message_end') this.#entries.set(event.entry.id, event.entry);\n if (this.#ours(event)) return this.#end('aborted');\n if (!this.#started) {\n this.#begin();\n continue;\n }\n const mapped = this.#mapper.event(event).filter((item) => item.t !== 'user');\n this.#out.push(...mapped);\n if (mapped.some((item) => item.t === 'run' && item.state === 'end'))\n return this.#end(this.#mapper.reason, this.#mapper.error);\n }\n return false;\n }\n}\n\nexport function createRemote(host: RemoteHost): AgentRemote & { dispose(): Promise<void> } {\n const now = host.now ?? Date.now;\n const streams = new Map<string, Stream>();\n let serial = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const sweep = () => {\n for (const [id, stream] of streams)\n if (now() - stream.touched > IDLE_MS) {\n streams.delete(id);\n void stream.finish();\n }\n if (!streams.size && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n const track = (id: string, stream: Stream) => {\n streams.set(id, stream);\n timer ??= setInterval(sweep, host.sweepMs ?? IDLE_MS / 4);\n };\n const watching = async (agentId: string | null, context: Context) => {\n const conversation = await host.conversation(agentId, context);\n const cancel = withCancel(BACKGROUND_CONTEXT);\n const events: AgentEventStream = await watchEvents(\n host.harness,\n conversation.id,\n cancel.context\n );\n const open = (onEvents: (batch: readonly AgentEvent[]) => void) => {\n const stream = new Stream<RemoteEvent>(\n async () => {\n await events.stop();\n cancel.cancel();\n },\n now(),\n host.limit ?? MAX_QUEUED,\n [{ t: 'end', reason: 'error', message: 'the stream fell too far behind' }]\n );\n const id = `s${++serial}-${crypto.randomUUID()}`;\n track(id, stream);\n events.start(async (batch) => onEvents(batch));\n return { id, stream };\n };\n return { conversation, snapshot: events.snapshot, open, stop: () => events.stop() };\n };\n return {\n async prompt(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const busy = Boolean(watched.snapshot.run);\n const response = await host.send(\n {\n text: request.text,\n whenBusy: request.whenBusy,\n requestId: null,\n ...(request.agentId ? { agentId: request.agentId } : {}),\n },\n context\n );\n if (!response.accepted) {\n await watched.stop();\n return {\n ok: false,\n code: response.error.code === 'busy' ? 'EBUSY' : 'EIO',\n message: response.error.message,\n };\n }\n const submission = Number(response.submissionId) as SubmissionId;\n const record = await (await host.harness.submission(submission, context))?.status(context);\n const delivered = deliveredOf(record, request.whenBusy, busy);\n const follower = new Follower(\n new Mapper(request.thinking),\n watched.snapshot.entries,\n submission,\n delivered,\n record\n );\n const { id, stream } = watched.open((batch) => {\n const ended = follower.take(batch);\n stream.push(follower.drain());\n if (ended) void stream.finish();\n });\n return { ok: true, stream: id, delivered, head: null, gap: false };\n },\n async watch(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const partial = watched.snapshot.generation?.message as never;\n const all = replayOf(watched.snapshot.entries, partial, request.thinking);\n const head = headOf(all);\n const at = request.since === null ? -1 : all.findIndex((e) => idOf(e) === request.since);\n const gap = request.since !== null && at < 0;\n const mapper = new Mapper(request.thinking);\n for (const entry of watched.snapshot.entries) mapper.entry(entry);\n mapper.partial(partial);\n const { id, stream } = watched.open((batch) =>\n stream.push(batch.flatMap((event) => followed(mapper, event)))\n );\n if (at >= 0) {\n const own = all[at] as RemoteEvent;\n const sinceId = idOf(own);\n let last = at;\n while (last + 1 < all.length && idOf(all[last + 1] as RemoteEvent) === sinceId) last++;\n const replay = own.t === 'text' || own.t === 'thinking' ? [own] : [];\n stream.push([...replay, ...all.slice(last + 1)]);\n }\n return { ok: true, stream: id, delivered: null, head, gap };\n },\n async next(id, waitMs, context) {\n const stream = streams.get(id);\n if (!stream) return { events: [], done: true };\n stream.touched = now();\n await stream.wait(Math.max(0, Math.min(waitMs, 30000)));\n context.abortSignal?.throwIfAborted();\n const events = stream.queue.splice(0);\n const done = stream.done && stream.queue.length === 0;\n if (done) streams.delete(id);\n return { events, done };\n },\n async close(id) {\n const stream = streams.get(id);\n streams.delete(id);\n await stream?.finish();\n },\n async abort(agentId, context) {\n const conversation = await host.conversation(agentId, context);\n const watched = await watchEvents(host.harness, conversation.id, context);\n const busy = Boolean(watched.snapshot.run);\n await watched.stop();\n if (busy) await conversation.abort(context);\n return { aborted: busy };\n },\n async dispose() {\n clearInterval(timer);\n timer = undefined;\n const all = [...streams.values()];\n streams.clear();\n await Promise.all(all.map((stream) => stream.finish()));\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context, ReplicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT, withCancel } from '@earendil-works/chord/context';\nimport {\n type AgentEvent,\n type AgentEventStream,\n type Conversation,\n type EntryRecord,\n type Harness,\n type SnapshotEvent,\n type SubmissionId,\n watchEvents,\n} from '@earendil-works/pi-durable';\nimport { holdText, type VaultState } from '../secrets/vault.ts';\nimport type { Delivered, SendMode, SendRequest, SendResponse } from '../services.ts';\nimport { type EndReason, idOf, Mapper, type RemoteEvent, replayOf } from './events.ts';\n\nexport const VAULT_BACK = 'the vault is answering again';\n\nexport function vaultNotices(\n vault: ReplicatedState<VaultState> | undefined,\n push: (events: RemoteEvent[]) => void\n): () => void {\n if (!vault) return () => undefined;\n let shown = vault.value ? holdText(vault.value) : undefined;\n let serial = 0;\n const notice = (text: string) => push([{ t: 'notice', id: `vault-${++serial}`, text }]);\n if (shown) notice(shown);\n return vault.subscribe((value) => {\n const next = holdText(value);\n if (next === shown) return;\n shown = next;\n notice(next ?? VAULT_BACK);\n });\n}\n\nexport const MAX_QUEUED = 10000;\nexport const IDLE_MS = 120000;\n\nexport type RemoteOpened =\n | { ok: true; stream: string; delivered: Delivered | null; head: string | null; gap: boolean }\n | { ok: false; code: string; message: string };\n\nexport interface RemoteBatch {\n events: RemoteEvent[];\n done: boolean;\n}\n\nexport interface AgentRemote {\n prompt(\n request: { agentId: string | null; text: string; whenBusy: SendMode; thinking: boolean },\n context: Context\n ): Promise<RemoteOpened>;\n watch(\n request: { agentId: string | null; since: string | null; thinking: boolean },\n context: Context\n ): Promise<RemoteOpened>;\n next(stream: string, waitMs: number, context: Context): Promise<RemoteBatch>;\n close(stream: string, context: Context): Promise<void>;\n abort(agentId: string | null, context: Context): Promise<{ aborted: boolean }>;\n}\n\nexport interface RemoteHost {\n harness: Harness;\n conversation(agentId: string | null, context: Context): Promise<Conversation>;\n send(request: SendRequest, context: Context): Promise<SendResponse>;\n vault?: ReplicatedState<VaultState>;\n now?: () => number;\n limit?: number;\n sweepMs?: number;\n}\n\nexport class Stream<T = RemoteEvent> {\n readonly queue: T[] = [];\n done = false;\n touched: number;\n #wake: (() => void) | undefined;\n readonly #stop: () => Promise<unknown>;\n readonly #limit: number;\n readonly #overflow: readonly T[];\n\n constructor(stop: () => Promise<unknown>, now: number, limit: number, overflow: readonly T[]) {\n this.#stop = stop;\n this.touched = now;\n this.#limit = limit;\n this.#overflow = overflow;\n }\n\n push(events: readonly T[]): void {\n if (this.done || !events.length) return;\n this.queue.push(...events);\n if (this.queue.length > this.#limit) {\n this.queue.length = 0;\n this.queue.push(...this.#overflow);\n void this.finish();\n }\n this.#wake?.();\n }\n\n async finish(): Promise<void> {\n if (this.done) return;\n this.done = true;\n this.#wake?.();\n await this.#stop().catch(() => undefined);\n }\n\n wait(ms: number, signal?: AbortSignal): Promise<void> {\n if (this.queue.length || this.done) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(done, ms);\n signal?.addEventListener('abort', done, { once: true });\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n this.#wake = () => {\n this.#wake = undefined;\n done();\n };\n });\n }\n}\n\nfunction deliveredOf(\n status: { status: string } | undefined,\n whenBusy: SendMode,\n busy: boolean\n): Delivered {\n if (status?.status === 'queued') return whenBusy === 'followUp' ? 'follow-up' : 'steer';\n return busy ? 'steer' : 'run';\n}\n\nfunction headOf(events: readonly RemoteEvent[]): string | null {\n for (let at = events.length - 1; at >= 0; at--) {\n const id = idOf(events[at] as RemoteEvent);\n if (id) return id;\n }\n return null;\n}\n\nexport function followed(mapper: Mapper, event: AgentEvent): RemoteEvent[] {\n return event.type === 'snapshot' ? resync(mapper, event) : mapper.event(event);\n}\n\nexport function resync(mapper: Mapper, snapshot: SnapshotEvent): RemoteEvent[] {\n const last = mapper.last();\n return [\n ...snapshot.entries.filter((entry) => entry.id > last).flatMap((entry) => mapper.entry(entry)),\n ...mapper.partial(snapshot.generation?.message as never),\n ];\n}\n\nfunction placedEntry(value: unknown): number | undefined {\n const entry = (value as { entry?: unknown } | undefined)?.entry;\n return typeof entry === 'number' ? entry : undefined;\n}\n\nexport class Follower {\n readonly #mapper: Mapper;\n readonly #entries: Map<number, EntryRecord>;\n readonly #submission: SubmissionId;\n readonly #delivered: Delivered;\n readonly #out: RemoteEvent[] = [];\n #user: number | undefined;\n #started = false;\n\n constructor(\n mapper: Mapper,\n entries: readonly EntryRecord[],\n submission: SubmissionId,\n delivered: Delivered,\n record: unknown\n ) {\n this.#mapper = mapper;\n this.#entries = new Map(entries.map((entry) => [entry.id, entry]));\n this.#submission = submission;\n this.#delivered = delivered;\n this.#user = placedEntry(record);\n }\n\n drain(): RemoteEvent[] {\n return this.#out.splice(0);\n }\n\n #begin(): void {\n const entry = this.#user === undefined ? undefined : this.#entries.get(this.#user);\n if (!entry) return;\n this.#started = true;\n this.#out.push(...this.#mapper.entry(entry, this.#delivered));\n }\n\n #end(reason: EndReason, message?: string): true {\n this.#out.push({ t: 'end', reason, ...(message ? { message } : {}) });\n return true;\n }\n\n #ours(event: AgentEvent): boolean {\n if (event.type !== 'submission' || event.record.id !== this.#submission) return false;\n this.#user = placedEntry(event.record) ?? this.#user;\n return this.#user === undefined && !['queued', 'placed'].includes(event.record.status);\n }\n\n take(batch: readonly AgentEvent[]): boolean {\n for (const event of batch) {\n if (event.type === 'message_end') this.#entries.set(event.entry.id, event.entry);\n if (this.#ours(event)) return this.#end('aborted');\n if (!this.#started) {\n this.#begin();\n continue;\n }\n const mapped = this.#mapper.event(event).filter((item) => item.t !== 'user');\n this.#out.push(...mapped);\n if (mapped.some((item) => item.t === 'run' && item.state === 'end'))\n return this.#end(this.#mapper.reason, this.#mapper.error);\n }\n return false;\n }\n}\n\nexport function createRemote(host: RemoteHost): AgentRemote & { dispose(): Promise<void> } {\n const now = host.now ?? Date.now;\n const streams = new Map<string, Stream>();\n let serial = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const sweep = () => {\n for (const [id, stream] of streams)\n if (now() - stream.touched > IDLE_MS) {\n streams.delete(id);\n void stream.finish();\n }\n if (!streams.size && timer !== undefined) {\n clearInterval(timer);\n timer = undefined;\n }\n };\n const track = (id: string, stream: Stream) => {\n streams.set(id, stream);\n timer ??= setInterval(sweep, host.sweepMs ?? IDLE_MS / 4);\n };\n const watching = async (agentId: string | null, context: Context) => {\n const conversation = await host.conversation(agentId, context);\n const cancel = withCancel(BACKGROUND_CONTEXT);\n const events: AgentEventStream = await watchEvents(\n host.harness,\n conversation.id,\n cancel.context\n );\n const open = (onEvents: (batch: readonly AgentEvent[]) => void) => {\n let off = () => undefined as void;\n const stream = new Stream<RemoteEvent>(\n async () => {\n off();\n await events.stop();\n cancel.cancel();\n },\n now(),\n host.limit ?? MAX_QUEUED,\n [{ t: 'end', reason: 'error', message: 'the stream fell too far behind' }]\n );\n const id = `s${++serial}-${crypto.randomUUID()}`;\n track(id, stream);\n off = vaultNotices(host.vault, (notices) => stream.push(notices));\n events.start(async (batch) => onEvents(batch));\n return { id, stream };\n };\n return { conversation, snapshot: events.snapshot, open, stop: () => events.stop() };\n };\n return {\n async prompt(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const busy = Boolean(watched.snapshot.run);\n const response = await host.send(\n {\n text: request.text,\n whenBusy: request.whenBusy,\n requestId: null,\n ...(request.agentId ? { agentId: request.agentId } : {}),\n },\n context\n );\n if (!response.accepted) {\n await watched.stop();\n return {\n ok: false,\n code: response.error.code === 'busy' ? 'EBUSY' : 'EIO',\n message: response.error.message,\n };\n }\n const submission = Number(response.submissionId) as SubmissionId;\n const record = await (await host.harness.submission(submission, context))?.status(context);\n const delivered = deliveredOf(record, request.whenBusy, busy);\n const follower = new Follower(\n new Mapper(request.thinking),\n watched.snapshot.entries,\n submission,\n delivered,\n record\n );\n const { id, stream } = watched.open((batch) => {\n const ended = follower.take(batch);\n stream.push(follower.drain());\n if (ended) void stream.finish();\n });\n return { ok: true, stream: id, delivered, head: null, gap: false };\n },\n async watch(request, context) {\n sweep();\n const watched = await watching(request.agentId, context);\n const partial = watched.snapshot.generation?.message as never;\n const all = replayOf(watched.snapshot.entries, partial, request.thinking);\n const head = headOf(all);\n const at = request.since === null ? -1 : all.findIndex((e) => idOf(e) === request.since);\n const gap = request.since !== null && at < 0;\n const mapper = new Mapper(request.thinking);\n for (const entry of watched.snapshot.entries) mapper.entry(entry);\n mapper.partial(partial);\n const { id, stream } = watched.open((batch) =>\n stream.push(batch.flatMap((event) => followed(mapper, event)))\n );\n if (at >= 0) {\n const own = all[at] as RemoteEvent;\n const sinceId = idOf(own);\n let last = at;\n while (last + 1 < all.length && idOf(all[last + 1] as RemoteEvent) === sinceId) last++;\n const replay = own.t === 'text' || own.t === 'thinking' ? [own] : [];\n stream.push([...replay, ...all.slice(last + 1)]);\n }\n return { ok: true, stream: id, delivered: null, head, gap };\n },\n async next(id, waitMs, context) {\n const stream = streams.get(id);\n if (!stream) return { events: [], done: true };\n stream.touched = now();\n await stream.wait(Math.max(0, Math.min(waitMs, 30000)));\n context.abortSignal?.throwIfAborted();\n const events = stream.queue.splice(0);\n const done = stream.done && stream.queue.length === 0;\n if (done) streams.delete(id);\n return { events, done };\n },\n async close(id) {\n const stream = streams.get(id);\n streams.delete(id);\n await stream?.finish();\n },\n async abort(agentId, context) {\n const conversation = await host.conversation(agentId, context);\n const watched = await watchEvents(host.harness, conversation.id, context);\n const busy = Boolean(watched.snapshot.run);\n await watched.stop();\n if (busy) await conversation.abort(context);\n return { aborted: busy };\n },\n async dispose() {\n clearInterval(timer);\n timer = undefined;\n const all = [...streams.values()];\n streams.clear();\n await Promise.all(all.map((stream) => stream.finish()));\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,150 @@
1
+ import {
2
+ defineExtension,
3
+ GenerationTask,
4
+ hook,
5
+ section,
6
+ ToolTask
7
+ } from "@earendil-works/pi-durable";
8
+ const SECRETS = "slicc-secrets";
9
+ const SECRETS_KIND = "slicc.secrets";
10
+ const SECRETS_COMMAND = "/secrets";
11
+ const SHELL_GRANTS = ["fnox", "exec", "--shell-grants", "--"];
12
+ const MODEL_LINE = "Credentials are env vars (`$NAME`) holding placeholders; `secret list` shows them and where each works. The vault fills in the real value only in the listed header, for the listed domain, method and path, over HTTPS (curl, git, fetch). Adding a rule means editing `/etc/fnox/config.toml` with sudo, which the user approves. `fnox exec` gives real values only for secrets the user enabled for exec, and the secrets listed as 'in every bash' already hold real values in your commands. Everything is logged, and stored values are replaced by their placeholder in everything you see; edit around placeholders, since files show them too. Never ask the user to paste a credential into the chat.";
13
+ function leaves(value, out, set) {
14
+ if (typeof value === "string") {
15
+ out.push({ get: () => value, set });
16
+ return;
17
+ }
18
+ if (Array.isArray(value)) {
19
+ value.forEach((item, at) => {
20
+ leaves(item, out, (next) => {
21
+ value[at] = next;
22
+ });
23
+ });
24
+ return;
25
+ }
26
+ if (value && typeof value === "object")
27
+ for (const [key, item] of Object.entries(value))
28
+ leaves(item, out, (next) => {
29
+ value[key] = next;
30
+ });
31
+ }
32
+ async function scrubResult(result, scrub) {
33
+ const content = [...result.content ?? []];
34
+ const diagnostics = [...result.diagnostics ?? []];
35
+ const details = result.details === void 0 ? void 0 : structuredClone(result.details);
36
+ const found = [];
37
+ content.forEach((item, at) => {
38
+ if (item.type === "text")
39
+ found.push({ get: () => item.text, set: (text) => void (content[at] = { ...item, text }) });
40
+ });
41
+ diagnostics.forEach((item, at) => {
42
+ found.push({
43
+ get: () => item.message,
44
+ set: (message) => void (diagnostics[at] = { ...item, message })
45
+ });
46
+ });
47
+ let replaced = details;
48
+ if (details !== void 0)
49
+ leaves(details, found, (next) => {
50
+ replaced = next;
51
+ });
52
+ const before = found.map((leaf) => leaf.get());
53
+ const after = await scrub.texts(before);
54
+ if (after.every((text, at) => text === before[at])) return void 0;
55
+ found.forEach((leaf, at) => {
56
+ leaf.set(after[at]);
57
+ });
58
+ const kept = result.content;
59
+ if (kept) kept.splice(0, kept.length, ...content);
60
+ return {
61
+ ...result,
62
+ ...kept ? { content: kept } : {},
63
+ ...result.diagnostics ? { diagnostics } : {},
64
+ ...replaced === void 0 ? {} : { details: replaced }
65
+ };
66
+ }
67
+ function where(name) {
68
+ const rules = name.rules.map(
69
+ (rule) => [
70
+ rule.domain,
71
+ rule.header ? `header ${rule.header}` : "",
72
+ rule.methods?.length ? rule.methods.join(" ") : "",
73
+ rule.paths?.length ? rule.paths.join(" ") : ""
74
+ ].filter(Boolean).join(", ")
75
+ );
76
+ const shell = name.exec === "shell" ? ["in every bash"] : [];
77
+ const parts = [...rules, ...shell];
78
+ return parts.length ? `: ${parts.join("; ")}` : "";
79
+ }
80
+ function renderSecrets(names) {
81
+ if (!names.length) return void 0;
82
+ const lines = names.map((name) => `- ${name.name} = ${name.placeholder}${where(name)}`);
83
+ return [MODEL_LINE, "", ...lines].join("\n");
84
+ }
85
+ function listedLine(names) {
86
+ return `The user listed secrets: ${names.length ? names.map((name) => name.name).join(", ") : "none"}.`;
87
+ }
88
+ function secretsEntry(names) {
89
+ return {
90
+ kind: SECRETS_KIND,
91
+ data: { names: structuredClone(names) },
92
+ model: [{ role: "user", content: listedLine(names), timestamp: Date.now() }]
93
+ };
94
+ }
95
+ function shellGrants(names) {
96
+ return (argv) => names.state.value.some((name) => name.exec === "shell") ? [...SHELL_GRANTS, ...argv] : argv;
97
+ }
98
+ function pinned(change) {
99
+ const list = change.extensions;
100
+ if (!Array.isArray(list) || list.some((extension) => extension.name === SECRETS)) return change;
101
+ return { ...change, extensions: [...list, { name: SECRETS }] };
102
+ }
103
+ async function repin(harness, storage, context) {
104
+ let fixed = 0;
105
+ let cursor;
106
+ do {
107
+ const page = await storage.scanConversations({}, 100, cursor, context);
108
+ for (const record of page.items) {
109
+ const conversation = await harness.conversation(record.id, context);
110
+ if (!conversation) continue;
111
+ const agent = await conversation.agent(context);
112
+ if (agent.extensions.some((extension) => extension.name === SECRETS)) continue;
113
+ await conversation.configure(pinned({ extensions: agent.extensions }), context);
114
+ fixed++;
115
+ }
116
+ cursor = page.next;
117
+ } while (cursor);
118
+ return fixed;
119
+ }
120
+ function secretsExtension(deps) {
121
+ return defineExtension({
122
+ name: SECRETS,
123
+ sections: [section("secrets", () => renderSecrets(deps.names.state.value))],
124
+ hooks: [
125
+ hook(ToolTask, { afterTool: (_call, result) => scrubResult(result, deps.vault) }),
126
+ hook(GenerationTask, {
127
+ beforeRequest: async () => {
128
+ await deps.vault.ready();
129
+ return void 0;
130
+ }
131
+ })
132
+ ]
133
+ });
134
+ }
135
+ export {
136
+ MODEL_LINE,
137
+ SECRETS,
138
+ SECRETS_COMMAND,
139
+ SECRETS_KIND,
140
+ SHELL_GRANTS,
141
+ listedLine,
142
+ pinned,
143
+ renderSecrets,
144
+ repin,
145
+ scrubResult,
146
+ secretsEntry,
147
+ secretsExtension,
148
+ shellGrants
149
+ };
150
+ //# sourceMappingURL=extension.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/secrets/extension.ts"],
4
+ "sourcesContent": ["import type { Context, JsonValue } from '@earendil-works/chord';\nimport {\n type AgentChange,\n type Cursor,\n defineExtension,\n type Extension,\n GenerationTask,\n type Harness,\n hook,\n type Storage,\n section,\n type ToolExecutionResult,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Names, SecretName } from './names.ts';\nimport type { Scrub } from './scrub.ts';\nimport type { Vault } from './vault.ts';\n\nexport const SECRETS = 'slicc-secrets';\nexport const SECRETS_KIND = 'slicc.secrets';\nexport const SECRETS_COMMAND = '/secrets';\nexport const SHELL_GRANTS = ['fnox', 'exec', '--shell-grants', '--'] as const;\n\nexport const MODEL_LINE =\n \"Credentials are env vars (`$NAME`) holding placeholders; `secret list` shows them and where each works. The vault fills in the real value only in the listed header, for the listed domain, method and path, over HTTPS (curl, git, fetch). Adding a rule means editing `/etc/fnox/config.toml` with sudo, which the user approves. `fnox exec` gives real values only for secrets the user enabled for exec, and the secrets listed as 'in every bash' already hold real values in your commands. Everything is logged, and stored values are replaced by their placeholder in everything you see; edit around placeholders, since files show them too. Never ask the user to paste a credential into the chat.\";\n\ntype Leaf = { get(): string; set(text: string): void };\n\nfunction leaves(value: JsonValue, out: Leaf[], set: (next: JsonValue) => void): void {\n if (typeof value === 'string') {\n out.push({ get: () => value, set });\n return;\n }\n if (Array.isArray(value)) {\n value.forEach((item, at) => {\n leaves(item, out, (next) => {\n value[at] = next;\n });\n });\n return;\n }\n if (value && typeof value === 'object')\n for (const [key, item] of Object.entries(value))\n leaves(item as JsonValue, out, (next) => {\n (value as Record<string, JsonValue>)[key] = next;\n });\n}\n\nexport async function scrubResult(\n result: ToolExecutionResult,\n scrub: Pick<Scrub, 'texts'>\n): Promise<ToolExecutionResult | undefined> {\n const content = [...(result.content ?? [])];\n const diagnostics = [...(result.diagnostics ?? [])];\n const details =\n result.details === undefined ? undefined : (structuredClone(result.details) as JsonValue);\n const found: Leaf[] = [];\n content.forEach((item, at) => {\n if (item.type === 'text')\n found.push({ get: () => item.text, set: (text) => void (content[at] = { ...item, text }) });\n });\n diagnostics.forEach((item, at) => {\n found.push({\n get: () => item.message,\n set: (message) => void (diagnostics[at] = { ...item, message }),\n });\n });\n let replaced = details;\n if (details !== undefined)\n leaves(details, found, (next) => {\n replaced = next;\n });\n const before = found.map((leaf) => leaf.get());\n const after = await scrub.texts(before);\n if (after.every((text, at) => text === before[at])) return undefined;\n found.forEach((leaf, at) => {\n leaf.set(after[at] as string);\n });\n const kept = result.content as ToolExecutionResult['content'] & unknown[];\n if (kept) kept.splice(0, kept.length, ...content);\n return {\n ...result,\n ...(kept ? { content: kept } : {}),\n ...(result.diagnostics ? { diagnostics } : {}),\n ...(replaced === undefined ? {} : { details: replaced }),\n };\n}\n\nfunction where(name: SecretName): string {\n const rules = name.rules.map((rule) =>\n [\n rule.domain,\n rule.header ? `header ${rule.header}` : '',\n rule.methods?.length ? rule.methods.join(' ') : '',\n rule.paths?.length ? rule.paths.join(' ') : '',\n ]\n .filter(Boolean)\n .join(', ')\n );\n const shell = name.exec === 'shell' ? ['in every bash'] : [];\n const parts = [...rules, ...shell];\n return parts.length ? `: ${parts.join('; ')}` : '';\n}\n\nexport function renderSecrets(names: readonly SecretName[]): string | undefined {\n if (!names.length) return undefined;\n const lines = names.map((name) => `- ${name.name} = ${name.placeholder}${where(name)}`);\n return [MODEL_LINE, '', ...lines].join('\\n');\n}\n\nexport function listedLine(names: readonly SecretName[]): string {\n return `The user listed secrets: ${names.length ? names.map((name) => name.name).join(', ') : 'none'}.`;\n}\n\nexport function secretsEntry(names: readonly SecretName[]) {\n return {\n kind: SECRETS_KIND,\n data: { names: structuredClone(names) as unknown as JsonValue },\n model: [{ role: 'user' as const, content: listedLine(names), timestamp: Date.now() }],\n };\n}\n\nexport function shellGrants(names: Pick<Names, 'state'>) {\n return (argv: readonly string[]): readonly string[] =>\n names.state.value.some((name) => name.exec === 'shell') ? [...SHELL_GRANTS, ...argv] : argv;\n}\n\nexport function pinned<T extends AgentChange>(change: T): T {\n const list = change.extensions;\n if (!Array.isArray(list) || list.some((extension) => extension.name === SECRETS)) return change;\n return { ...change, extensions: [...list, { name: SECRETS }] };\n}\n\nexport async function repin(\n harness: Pick<Harness, 'conversation'>,\n storage: Pick<Storage, 'scanConversations'>,\n context: Context\n): Promise<number> {\n let fixed = 0;\n let cursor: Cursor | undefined;\n do {\n const page = await storage.scanConversations({}, 100, cursor, context);\n for (const record of page.items) {\n const conversation = await harness.conversation(record.id, context);\n if (!conversation) continue;\n const agent = await conversation.agent(context);\n if (agent.extensions.some((extension) => extension.name === SECRETS)) continue;\n await conversation.configure(pinned({ extensions: agent.extensions }), context);\n fixed++;\n }\n cursor = page.next;\n } while (cursor);\n return fixed;\n}\n\nexport function secretsExtension(deps: {\n names: Pick<Names, 'state'>;\n vault: Pick<Vault, 'ready' | 'texts'>;\n}): Extension {\n return defineExtension({\n name: SECRETS,\n sections: [section('secrets', () => renderSecrets(deps.names.state.value))],\n hooks: [\n hook(ToolTask, { afterTool: (_call, result) => scrubResult(result, deps.vault) }),\n hook(GenerationTask, {\n beforeRequest: async () => {\n await deps.vault.ready();\n return undefined;\n },\n }),\n ],\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,72 @@
1
+ import { replicatedState } from "@earendil-works/chord";
2
+ const LIST = "secret list --json";
3
+ function strings(value) {
4
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : void 0;
5
+ }
6
+ function ruleOf(value) {
7
+ const rule = value;
8
+ if (typeof rule?.domain !== "string") return [];
9
+ const methods = strings(rule.methods);
10
+ const paths = strings(rule.paths);
11
+ return [
12
+ {
13
+ domain: rule.domain,
14
+ ...typeof rule.header === "string" ? { header: rule.header } : {},
15
+ ...methods ? { methods } : {},
16
+ ...paths ? { paths } : {}
17
+ }
18
+ ];
19
+ }
20
+ const EXEC = /* @__PURE__ */ new Set(["any", "argv", "shell"]);
21
+ function parseNames(text) {
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(text);
25
+ } catch {
26
+ return [];
27
+ }
28
+ const list = parsed?.secrets;
29
+ if (!Array.isArray(list)) return [];
30
+ return list.flatMap((item) => {
31
+ if (typeof item?.name !== "string" || typeof item.placeholder !== "string") return [];
32
+ const exec = EXEC.has(item.exec) ? item.exec : false;
33
+ const rules = Array.isArray(item.rules) ? item.rules.flatMap(ruleOf) : [];
34
+ return [{ name: item.name, placeholder: item.placeholder, exec, rules }];
35
+ });
36
+ }
37
+ async function listNames(env, context) {
38
+ let out = "";
39
+ const ran = await env.exec(
40
+ LIST,
41
+ {
42
+ onOutput: (text, _using, info) => {
43
+ if (info?.stream !== "stderr") out += text;
44
+ }
45
+ },
46
+ context
47
+ ).catch(() => void 0);
48
+ return ran?.ok && ran.value.exitCode === 0 ? parseNames(out) : [];
49
+ }
50
+ function secretNames(env, vault) {
51
+ const state = replicatedState([]);
52
+ let version = vault.state.value.version;
53
+ let lock = Promise.resolve();
54
+ const refresh = (context) => {
55
+ const run = lock.then(async () => state.replace(context, await listNames(env, context)));
56
+ lock = run.catch(() => void 0);
57
+ return run;
58
+ };
59
+ const off = vault.state.subscribe((value, context) => {
60
+ if (value.version === version) return;
61
+ version = value.version;
62
+ void refresh(context).catch(() => void 0);
63
+ });
64
+ return { state, refresh, close: off };
65
+ }
66
+ export {
67
+ LIST,
68
+ listNames,
69
+ parseNames,
70
+ secretNames
71
+ };
72
+ //# sourceMappingURL=names.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/secrets/names.ts"],
4
+ "sourcesContent": ["import { type Context, type MutableReplicatedState, replicatedState } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport type { Vault } from './vault.ts';\n\nexport interface SecretRule {\n domain: string;\n header?: string;\n methods?: string[];\n paths?: string[];\n}\n\nexport interface SecretName {\n name: string;\n placeholder: string;\n exec: false | 'any' | 'argv' | 'shell';\n rules: SecretRule[];\n}\n\nexport const LIST = 'secret list --json';\n\nfunction strings(value: unknown): string[] | undefined {\n return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : undefined;\n}\n\nfunction ruleOf(value: unknown): SecretRule[] {\n const rule = value as Record<string, unknown> | null;\n if (typeof rule?.domain !== 'string') return [];\n const methods = strings(rule.methods);\n const paths = strings(rule.paths);\n return [\n {\n domain: rule.domain,\n ...(typeof rule.header === 'string' ? { header: rule.header } : {}),\n ...(methods ? { methods } : {}),\n ...(paths ? { paths } : {}),\n },\n ];\n}\n\nconst EXEC = new Set(['any', 'argv', 'shell']);\n\nexport function parseNames(text: string): SecretName[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [];\n }\n const list = (parsed as { secrets?: unknown } | null)?.secrets;\n if (!Array.isArray(list)) return [];\n return list.flatMap((item: Record<string, unknown> | null) => {\n if (typeof item?.name !== 'string' || typeof item.placeholder !== 'string') return [];\n const exec = EXEC.has(item.exec as string) ? (item.exec as SecretName['exec']) : false;\n const rules = Array.isArray(item.rules) ? item.rules.flatMap(ruleOf) : [];\n return [{ name: item.name, placeholder: item.placeholder, exec, rules }];\n });\n}\n\nexport async function listNames(env: ExecutionEnv, context: Context): Promise<SecretName[]> {\n let out = '';\n const ran = await env\n .exec(\n LIST,\n {\n onOutput: (text, _using, info) => {\n if (info?.stream !== 'stderr') out += text;\n },\n },\n context\n )\n .catch(() => undefined);\n return ran?.ok && ran.value.exitCode === 0 ? parseNames(out) : [];\n}\n\nexport interface Names {\n readonly state: MutableReplicatedState<SecretName[]>;\n refresh(context: Context): Promise<void>;\n close(): void;\n}\n\nexport function secretNames(env: ExecutionEnv, vault: Pick<Vault, 'state'>): Names {\n const state = replicatedState<SecretName[]>([]);\n let version = vault.state.value.version;\n let lock: Promise<unknown> = Promise.resolve();\n const refresh = (context: Context) => {\n const run = lock.then(async () => state.replace(context, await listNames(env, context)));\n lock = run.catch(() => undefined);\n return run;\n };\n const off = vault.state.subscribe((value, context) => {\n if (value.version === version) return;\n version = value.version;\n void refresh(context).catch(() => undefined);\n });\n return { state, refresh, close: off };\n}\n"],
5
+ "mappings": "AAAA,SAAoD,uBAAuB;AAkBpE,MAAM,OAAO;AAEpB,SAAS,QAAQ,OAAsC;AACrD,SAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,OAAO,CAAC,SAAS,OAAO,SAAS,QAAQ,IAAI;AACnF;AAEA,SAAS,OAAO,OAA8B;AAC5C,QAAM,OAAO;AACb,MAAI,OAAO,MAAM,WAAW,SAAU,QAAO,CAAC;AAC9C,QAAM,UAAU,QAAQ,KAAK,OAAO;AACpC,QAAM,QAAQ,QAAQ,KAAK,KAAK;AAChC,SAAO;AAAA,IACL;AAAA,MACE,QAAQ,KAAK;AAAA,MACb,GAAI,OAAO,KAAK,WAAW,WAAW,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MACjE,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7B,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,MAAM,OAAO,oBAAI,IAAI,CAAC,OAAO,QAAQ,OAAO,CAAC;AAEtC,SAAS,WAAW,MAA4B;AACrD,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,IAAI;AAAA,EAC1B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,OAAQ,QAAyC;AACvD,MAAI,CAAC,MAAM,QAAQ,IAAI,EAAG,QAAO,CAAC;AAClC,SAAO,KAAK,QAAQ,CAAC,SAAyC;AAC5D,QAAI,OAAO,MAAM,SAAS,YAAY,OAAO,KAAK,gBAAgB,SAAU,QAAO,CAAC;AACpF,UAAM,OAAO,KAAK,IAAI,KAAK,IAAc,IAAK,KAAK,OAA8B;AACjF,UAAM,QAAQ,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,MAAM,QAAQ,MAAM,IAAI,CAAC;AACxE,WAAO,CAAC,EAAE,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,MAAM,MAAM,CAAC;AAAA,EACzE,CAAC;AACH;AAEA,eAAsB,UAAU,KAAmB,SAAyC;AAC1F,MAAI,MAAM;AACV,QAAM,MAAM,MAAM,IACf;AAAA,IACC;AAAA,IACA;AAAA,MACE,UAAU,CAAC,MAAM,QAAQ,SAAS;AAChC,YAAI,MAAM,WAAW,SAAU,QAAO;AAAA,MACxC;AAAA,IACF;AAAA,IACA;AAAA,EACF,EACC,MAAM,MAAM,MAAS;AACxB,SAAO,KAAK,MAAM,IAAI,MAAM,aAAa,IAAI,WAAW,GAAG,IAAI,CAAC;AAClE;AAQO,SAAS,YAAY,KAAmB,OAAoC;AACjF,QAAM,QAAQ,gBAA8B,CAAC,CAAC;AAC9C,MAAI,UAAU,MAAM,MAAM,MAAM;AAChC,MAAI,OAAyB,QAAQ,QAAQ;AAC7C,QAAM,UAAU,CAAC,YAAqB;AACpC,UAAM,MAAM,KAAK,KAAK,YAAY,MAAM,QAAQ,SAAS,MAAM,UAAU,KAAK,OAAO,CAAC,CAAC;AACvF,WAAO,IAAI,MAAM,MAAM,MAAS;AAChC,WAAO;AAAA,EACT;AACA,QAAM,MAAM,MAAM,MAAM,UAAU,CAAC,OAAO,YAAY;AACpD,QAAI,MAAM,YAAY,QAAS;AAC/B,cAAU,MAAM;AAChB,SAAK,QAAQ,OAAO,EAAE,MAAM,MAAM,MAAS;AAAA,EAC7C,CAAC;AACD,SAAO,EAAE,OAAO,SAAS,OAAO,IAAI;AACtC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,36 @@
1
+ const EMPTY = new Uint8Array();
2
+ const IDENTITY = {
3
+ texts: async (texts) => [...texts],
4
+ stream: () => ({ push: async (chunk) => chunk, end: async () => EMPTY, drop: () => void 0 })
5
+ };
6
+ let current = IDENTITY;
7
+ function scrubWith(next) {
8
+ current = next;
9
+ }
10
+ function scrubTexts(texts) {
11
+ return texts.length ? current.texts(texts) : Promise.resolve([]);
12
+ }
13
+ async function scrubText(text) {
14
+ return (await scrubTexts([text]))[0];
15
+ }
16
+ async function scrubItems(items) {
17
+ const scrubbed = await scrubTexts(items.map((item) => item.text ?? ""));
18
+ return items.map((item, at) => item.type === "text" ? { ...item, text: scrubbed[at] } : item);
19
+ }
20
+ async function scrubbedOutput(api, text) {
21
+ try {
22
+ api.output(await scrubText(text));
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ export {
29
+ IDENTITY,
30
+ scrubItems,
31
+ scrubText,
32
+ scrubTexts,
33
+ scrubWith,
34
+ scrubbedOutput
35
+ };
36
+ //# sourceMappingURL=scrub.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/secrets/scrub.ts"],
4
+ "sourcesContent": ["export type Scrubbable = string | Uint8Array;\n\nexport interface ScrubStream {\n push(chunk: Uint8Array): Promise<Uint8Array>;\n end(): Promise<Uint8Array>;\n drop(): void;\n}\n\nexport interface Scrub {\n texts<T extends Scrubbable>(texts: readonly T[]): Promise<T[]>;\n stream(): ScrubStream;\n}\n\nconst EMPTY = new Uint8Array();\n\nexport const IDENTITY: Scrub = {\n texts: async (texts) => [...texts],\n stream: () => ({ push: async (chunk) => chunk, end: async () => EMPTY, drop: () => undefined }),\n};\n\nlet current: Scrub = IDENTITY;\n\nexport function scrubWith(next: Scrub): void {\n current = next;\n}\n\nexport function scrubTexts<T extends Scrubbable>(texts: readonly T[]): Promise<T[]> {\n return texts.length ? current.texts(texts) : Promise.resolve([]);\n}\n\nexport async function scrubText(text: string): Promise<string> {\n return (await scrubTexts([text]))[0] as string;\n}\n\nexport async function scrubItems<T extends { type: string; text?: string }>(\n items: readonly T[]\n): Promise<T[]> {\n const scrubbed = await scrubTexts(items.map((item) => item.text ?? ''));\n return items.map((item, at) => (item.type === 'text' ? { ...item, text: scrubbed[at] } : item));\n}\n\nexport async function scrubbedOutput(\n api: { output(text: string): unknown },\n text: string\n): Promise<boolean> {\n try {\n api.output(await scrubText(text));\n return true;\n } catch {\n return false;\n }\n}\n"],
5
+ "mappings": "AAaA,MAAM,QAAQ,IAAI,WAAW;AAEtB,MAAM,WAAkB;AAAA,EAC7B,OAAO,OAAO,UAAU,CAAC,GAAG,KAAK;AAAA,EACjC,QAAQ,OAAO,EAAE,MAAM,OAAO,UAAU,OAAO,KAAK,YAAY,OAAO,MAAM,MAAM,OAAU;AAC/F;AAEA,IAAI,UAAiB;AAEd,SAAS,UAAU,MAAmB;AAC3C,YAAU;AACZ;AAEO,SAAS,WAAiC,OAAmC;AAClF,SAAO,MAAM,SAAS,QAAQ,MAAM,KAAK,IAAI,QAAQ,QAAQ,CAAC,CAAC;AACjE;AAEA,eAAsB,UAAU,MAA+B;AAC7D,UAAQ,MAAM,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;AACrC;AAEA,eAAsB,WACpB,OACc;AACd,QAAM,WAAW,MAAM,WAAW,MAAM,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;AACtE,SAAO,MAAM,IAAI,CAAC,MAAM,OAAQ,KAAK,SAAS,SAAS,EAAE,GAAG,MAAM,MAAM,SAAS,EAAE,EAAE,IAAI,IAAK;AAChG;AAEA,eAAsB,eACpB,KACA,MACkB;AAClB,MAAI;AACF,QAAI,OAAO,MAAM,UAAU,IAAI,CAAC;AAChC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,161 @@
1
+ import { replicatedState } from "@earendil-works/chord";
2
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
3
+ const WAITING_MS = 2e3;
4
+ const WAITING = "waiting for the vault";
5
+ const LOCKS = ["no", "scrubbing", "passkey"];
6
+ const EMPTY = new Uint8Array();
7
+ function pairOf(value) {
8
+ const pair = value;
9
+ if (pair === null) return { needed: false, version: 0 };
10
+ if (typeof pair?.needed !== "boolean" || typeof pair.version !== "number") return void 0;
11
+ return { needed: pair.needed, version: pair.version };
12
+ }
13
+ class Vault {
14
+ state;
15
+ #pending = /* @__PURE__ */ new Map();
16
+ #streams = /* @__PURE__ */ new Set();
17
+ #waiters = /* @__PURE__ */ new Set();
18
+ #now;
19
+ #port;
20
+ #pair = { needed: false, version: 0 };
21
+ #locked = "no";
22
+ #seen = 0;
23
+ #serial = 0;
24
+ #streamSerial = 0;
25
+ constructor(now = Date.now) {
26
+ this.#now = now;
27
+ this.state = replicatedState(this.#snapshot());
28
+ }
29
+ get needed() {
30
+ return this.#pair.needed || this.#seen > this.#pair.version;
31
+ }
32
+ connect(port, scrub) {
33
+ const pair = scrub === void 0 ? void 0 : pairOf(scrub);
34
+ if (pair) this.#pair = pair;
35
+ if (port && port !== this.#port) {
36
+ if (this.#port) this.#port.onmessage = null;
37
+ this.#port = port;
38
+ port.onmessage = (event) => this.#answer(event.data);
39
+ port.start?.();
40
+ const resend = [...this.#pending.values()];
41
+ const opening = new Set(
42
+ resend.filter((item) => item.message.op === "redactStream").map((item) => item.message.stream)
43
+ );
44
+ for (const stream of this.#streams)
45
+ if (!opening.has(stream)) void this.#call("redactStream", { stream });
46
+ for (const item of resend) port.postMessage(item.message);
47
+ }
48
+ this.#refresh();
49
+ }
50
+ ready() {
51
+ if (!this.#holding()) return Promise.resolve();
52
+ return new Promise((resolve) => this.#waiters.add(resolve));
53
+ }
54
+ async texts(texts) {
55
+ if (!this.needed) return [...texts];
56
+ const answer = await this.#call("redact", { texts: [...texts] });
57
+ return answer.texts;
58
+ }
59
+ stream() {
60
+ let stream;
61
+ return {
62
+ push: (chunk) => {
63
+ if (stream === void 0 && !this.needed) return Promise.resolve(chunk);
64
+ if (stream === void 0) {
65
+ stream = ++this.#streamSerial;
66
+ this.#streams.add(stream);
67
+ void this.#call("redactStream", { stream });
68
+ }
69
+ return this.#call("push", { stream, chunk });
70
+ },
71
+ end: async () => {
72
+ if (stream === void 0) return EMPTY;
73
+ const tail = await this.#call("end", { stream });
74
+ this.#streams.delete(stream);
75
+ return tail;
76
+ },
77
+ drop: () => {
78
+ if (stream === void 0) return;
79
+ const dropped = stream;
80
+ this.#streams.delete(dropped);
81
+ for (const [id, item] of this.#pending)
82
+ if (item.message.stream === dropped && item.message.op !== "redactStream") {
83
+ this.#pending.delete(id);
84
+ item.resolve(EMPTY);
85
+ }
86
+ void this.#call("drop", { stream: dropped });
87
+ }
88
+ };
89
+ }
90
+ #call(op, fields) {
91
+ const message = { id: ++this.#serial, op, ...fields };
92
+ const result = new Promise((resolve, reject) => {
93
+ this.#pending.set(message.id, { message, at: this.#now(), resolve, reject });
94
+ });
95
+ this.#port?.postMessage(message);
96
+ setTimeout(() => this.#refresh(), WAITING_MS);
97
+ this.#refresh();
98
+ return result;
99
+ }
100
+ #answer(data) {
101
+ if (!data || typeof data !== "object") return;
102
+ const pair = "scrub" in data ? pairOf(data.scrub) : void 0;
103
+ if (pair) this.#pair = pair;
104
+ if (LOCKS.includes(data.locked)) this.#locked = data.locked;
105
+ const item = typeof data.id === "number" ? this.#pending.get(data.id) : void 0;
106
+ if (item) {
107
+ this.#pending.delete(data.id);
108
+ const version = data.ok?.version;
109
+ if (typeof version === "number") this.#seen = Math.max(this.#seen, version);
110
+ if (data.error) item.reject(new Error(data.error.message ?? "the vault refused"));
111
+ else item.resolve(data.ok);
112
+ }
113
+ this.#refresh();
114
+ }
115
+ #waiting() {
116
+ const now = this.#now();
117
+ return [...this.#pending.values()].some((item) => now - item.at >= WAITING_MS);
118
+ }
119
+ #holding() {
120
+ return this.needed && (!this.#port || this.#waiting() || this.#locked === "passkey");
121
+ }
122
+ #snapshot() {
123
+ return {
124
+ port: !this.#port ? "none" : this.#waiting() ? "waiting" : "ready",
125
+ needed: this.needed,
126
+ version: this.#pair.version,
127
+ locked: this.#locked,
128
+ held: this.#pending.size
129
+ };
130
+ }
131
+ #refresh() {
132
+ const next = this.#snapshot();
133
+ const before = this.state.value;
134
+ if (Object.entries(next).some(([key, value]) => before[key] !== value))
135
+ this.state.replace(BACKGROUND_CONTEXT, next);
136
+ if (this.#holding()) return;
137
+ for (const resolve of this.#waiters) resolve();
138
+ this.#waiters.clear();
139
+ }
140
+ }
141
+ function holdText(state) {
142
+ if (!state.needed) return void 0;
143
+ if (state.locked === "passkey") return "unlock the vault to continue";
144
+ return state.port === "ready" ? void 0 : WAITING;
145
+ }
146
+ function listenVault(scope, vault) {
147
+ scope.addEventListener("message", ({ data }) => {
148
+ const message = data;
149
+ if (!message || typeof message !== "object" || !("vault" in message || "scrub" in message))
150
+ return;
151
+ vault.connect(message.vault, message.scrub);
152
+ });
153
+ }
154
+ export {
155
+ Vault,
156
+ WAITING,
157
+ WAITING_MS,
158
+ holdText,
159
+ listenVault
160
+ };
161
+ //# sourceMappingURL=vault.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/secrets/vault.ts"],
4
+ "sourcesContent": ["import { type MutableReplicatedState, replicatedState } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Scrub, Scrubbable, ScrubStream } from './scrub.ts';\n\nexport type ScrubPair = { needed: boolean; version: number };\nexport type Locked = 'no' | 'scrubbing' | 'passkey';\n\nexport interface VaultState {\n port: 'none' | 'ready' | 'waiting';\n needed: boolean;\n version: number;\n locked: Locked;\n held: number;\n}\n\nexport const WAITING_MS = 2000;\nexport const WAITING = 'waiting for the vault';\n\ntype Message = Record<string, unknown> & { id: number; op: string };\n\ninterface Pending {\n message: Message;\n at: number;\n resolve(value: unknown): void;\n reject(error: Error): void;\n}\n\ntype Answer = {\n id?: unknown;\n ok?: unknown;\n error?: { message?: string };\n scrub?: unknown;\n locked?: unknown;\n};\n\nconst LOCKS: readonly unknown[] = ['no', 'scrubbing', 'passkey'];\n\nconst EMPTY = new Uint8Array();\n\nfunction pairOf(value: unknown): ScrubPair | undefined {\n const pair = value as Partial<ScrubPair> | null;\n if (pair === null) return { needed: false, version: 0 };\n if (typeof pair?.needed !== 'boolean' || typeof pair.version !== 'number') return undefined;\n return { needed: pair.needed, version: pair.version };\n}\n\nexport class Vault implements Scrub {\n readonly state: MutableReplicatedState<VaultState>;\n readonly #pending = new Map<number, Pending>();\n readonly #streams = new Set<number>();\n readonly #waiters = new Set<() => void>();\n readonly #now: () => number;\n #port: MessagePort | undefined;\n #pair: ScrubPair = { needed: false, version: 0 };\n #locked: Locked = 'no';\n #seen = 0;\n #serial = 0;\n #streamSerial = 0;\n\n constructor(now: () => number = Date.now) {\n this.#now = now;\n this.state = replicatedState<VaultState>(this.#snapshot());\n }\n\n get needed(): boolean {\n return this.#pair.needed || this.#seen > this.#pair.version;\n }\n\n connect(port: MessagePort | undefined, scrub: unknown): void {\n const pair = scrub === undefined ? undefined : pairOf(scrub);\n if (pair) this.#pair = pair;\n if (port && port !== this.#port) {\n if (this.#port) this.#port.onmessage = null;\n this.#port = port;\n port.onmessage = (event) => this.#answer(event.data as Answer);\n port.start?.();\n const resend = [...this.#pending.values()];\n const opening = new Set(\n resend\n .filter((item) => item.message.op === 'redactStream')\n .map((item) => item.message.stream)\n );\n for (const stream of this.#streams)\n if (!opening.has(stream)) void this.#call('redactStream', { stream });\n for (const item of resend) port.postMessage(item.message);\n }\n this.#refresh();\n }\n\n ready(): Promise<void> {\n if (!this.#holding()) return Promise.resolve();\n return new Promise((resolve) => this.#waiters.add(resolve));\n }\n\n async texts<T extends Scrubbable>(texts: readonly T[]): Promise<T[]> {\n if (!this.needed) return [...texts];\n const answer = (await this.#call('redact', { texts: [...texts] })) as {\n texts: T[];\n version?: number;\n };\n return answer.texts;\n }\n\n stream(): ScrubStream {\n let stream: number | undefined;\n return {\n push: (chunk) => {\n if (stream === undefined && !this.needed) return Promise.resolve(chunk);\n if (stream === undefined) {\n stream = ++this.#streamSerial;\n this.#streams.add(stream);\n void this.#call('redactStream', { stream });\n }\n return this.#call('push', { stream, chunk }) as Promise<Uint8Array>;\n },\n end: async () => {\n if (stream === undefined) return EMPTY;\n const tail = (await this.#call('end', { stream })) as Uint8Array;\n this.#streams.delete(stream);\n return tail;\n },\n drop: () => {\n if (stream === undefined) return;\n const dropped = stream;\n this.#streams.delete(dropped);\n for (const [id, item] of this.#pending)\n if (item.message.stream === dropped && item.message.op !== 'redactStream') {\n this.#pending.delete(id);\n item.resolve(EMPTY);\n }\n void this.#call('drop', { stream: dropped });\n },\n };\n }\n\n #call(op: string, fields: Record<string, unknown>): Promise<unknown> {\n const message: Message = { id: ++this.#serial, op, ...fields };\n const result = new Promise((resolve, reject) => {\n this.#pending.set(message.id, { message, at: this.#now(), resolve, reject });\n });\n this.#port?.postMessage(message);\n setTimeout(() => this.#refresh(), WAITING_MS);\n this.#refresh();\n return result;\n }\n\n #answer(data: Answer): void {\n if (!data || typeof data !== 'object') return;\n const pair = 'scrub' in data ? pairOf(data.scrub) : undefined;\n if (pair) this.#pair = pair;\n if (LOCKS.includes(data.locked)) this.#locked = data.locked as Locked;\n const item = typeof data.id === 'number' ? this.#pending.get(data.id) : undefined;\n if (item) {\n this.#pending.delete(data.id as number);\n const version = (data.ok as { version?: unknown } | undefined)?.version;\n if (typeof version === 'number') this.#seen = Math.max(this.#seen, version);\n if (data.error) item.reject(new Error(data.error.message ?? 'the vault refused'));\n else item.resolve(data.ok);\n }\n this.#refresh();\n }\n\n #waiting(): boolean {\n const now = this.#now();\n return [...this.#pending.values()].some((item) => now - item.at >= WAITING_MS);\n }\n\n #holding(): boolean {\n return this.needed && (!this.#port || this.#waiting() || this.#locked === 'passkey');\n }\n\n #snapshot(): VaultState {\n return {\n port: !this.#port ? 'none' : this.#waiting() ? 'waiting' : 'ready',\n needed: this.needed,\n version: this.#pair.version,\n locked: this.#locked,\n held: this.#pending.size,\n };\n }\n\n #refresh(): void {\n const next = this.#snapshot();\n const before = this.state.value;\n if (Object.entries(next).some(([key, value]) => before[key as keyof VaultState] !== value))\n this.state.replace(BACKGROUND_CONTEXT, next);\n if (this.#holding()) return;\n for (const resolve of this.#waiters) resolve();\n this.#waiters.clear();\n }\n}\n\nexport function holdText(state: VaultState): string | undefined {\n if (!state.needed) return undefined;\n if (state.locked === 'passkey') return 'unlock the vault to continue';\n return state.port === 'ready' ? undefined : WAITING;\n}\n\nexport function listenVault(\n scope: { addEventListener(type: 'message', listener: (event: MessageEvent) => void): void },\n vault: Vault\n): void {\n scope.addEventListener('message', ({ data }) => {\n const message = data as { vault?: MessagePort; scrub?: unknown } | null;\n if (!message || typeof message !== 'object' || !('vault' in message || 'scrub' in message))\n return;\n vault.connect(message.vault, message.scrub);\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }