@tangle-network/agent-app 0.60.181 → 0.60.182
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/dist/assistant/index.js +12 -12
- package/dist/chat-routes/index.js +23 -23
- package/dist/chat-store/index.js +4 -4
- package/dist/{chunk-6ZUVYAKB.js → chunk-6O2NQ7WD.js} +9 -9
- package/dist/{chunk-IIE3K2Y5.js → chunk-EOPFHXEA.js} +1 -1
- package/dist/{chunk-PYPWEXTD.js → chunk-J26IXWFK.js} +12 -12
- package/dist/chunk-LTWJYNEC.js +100 -0
- package/dist/chunk-LTWJYNEC.js.map +1 -0
- package/dist/{chunk-YR6TZ777.js → chunk-TNWDYAFX.js} +6 -6
- package/dist/{chunk-T5PPQPOD.js → chunk-TXUAGSFR.js} +1 -1
- package/dist/{chunk-T5PPQPOD.js.map → chunk-TXUAGSFR.js.map} +1 -1
- package/dist/{chunk-XNKLQG26.js → chunk-ZGUXI5SI.js} +5 -5
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/model-resolution/allow-list.d.ts +64 -0
- package/dist/model-resolution/index.d.ts +1 -0
- package/dist/model-resolution/index.js +28 -0
- package/dist/model-resolution/index.js.map +1 -1
- package/dist/profile/index.js +2 -1
- package/dist/profile/index.js.map +1 -1
- package/dist/public-consultation/index.d.ts +1 -1
- package/dist/public-consultation/index.js +8 -8
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +26 -2
- package/dist/skills/marketplace-drizzle.d.ts +121 -0
- package/dist/skills/marketplace-drizzle.js +34 -0
- package/dist/skills/marketplace-drizzle.js.map +1 -0
- package/dist/skills/marketplace.d.ts +104 -0
- package/dist/skills-placement/index.js +2 -1
- package/dist/skills-placement/index.js.map +1 -1
- package/dist/spend/delegate-budget.d.ts +59 -0
- package/dist/spend/index.d.ts +1 -0
- package/dist/spend/index.js +43 -0
- package/dist/spend/index.js.map +1 -1
- package/dist/stream/index.js +2 -2
- package/dist/toolbelt/index.d.ts +80 -0
- package/dist/toolbelt/index.js +178 -0
- package/dist/toolbelt/index.js.map +1 -0
- package/dist/web-react/index.js +25 -25
- package/package.json +11 -1
- /package/dist/{chunk-6ZUVYAKB.js.map → chunk-6O2NQ7WD.js.map} +0 -0
- /package/dist/{chunk-IIE3K2Y5.js.map → chunk-EOPFHXEA.js.map} +0 -0
- /package/dist/{chunk-PYPWEXTD.js.map → chunk-J26IXWFK.js.map} +0 -0
- /package/dist/{chunk-YR6TZ777.js.map → chunk-TNWDYAFX.js.map} +0 -0
- /package/dist/{chunk-XNKLQG26.js.map → chunk-ZGUXI5SI.js.map} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle/D1 adapter for the cross-agent skill marketplace — the durable
|
|
3
|
+
* store shape shared by every product that publishes or reads a shared
|
|
4
|
+
* catalog (see `./skills` for the entry format and merge contract).
|
|
5
|
+
*
|
|
6
|
+
* Kept on its own subpath so `./skills` stays substrate-free: this is the
|
|
7
|
+
* only marketplace entry that touches the drizzle peer, mirroring
|
|
8
|
+
* `./agent-profiles/drizzle`.
|
|
9
|
+
*/
|
|
10
|
+
import type { MarketplaceStore } from './marketplace';
|
|
11
|
+
/** Any drizzle database instance (structural — the adapter only uses select
|
|
12
|
+
* over the standard table shape). */
|
|
13
|
+
type DrizzleLike = any;
|
|
14
|
+
/** The standard marketplace table: one row per namespaced `product:id`.
|
|
15
|
+
* Create it in a drizzle/D1 database, publish rows via
|
|
16
|
+
* `marketplaceRowsFor`, read via {@link createDrizzleMarketplaceStore}. */
|
|
17
|
+
export declare function marketplaceSkillTable(tableName?: string): import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
18
|
+
name: string;
|
|
19
|
+
schema: undefined;
|
|
20
|
+
columns: {
|
|
21
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
22
|
+
name: "id";
|
|
23
|
+
tableName: string;
|
|
24
|
+
dataType: "string";
|
|
25
|
+
columnType: "SQLiteText";
|
|
26
|
+
data: string;
|
|
27
|
+
driverParam: string;
|
|
28
|
+
notNull: true;
|
|
29
|
+
hasDefault: false;
|
|
30
|
+
isPrimaryKey: true;
|
|
31
|
+
isAutoincrement: false;
|
|
32
|
+
hasRuntimeDefault: false;
|
|
33
|
+
enumValues: [string, ...string[]];
|
|
34
|
+
baseColumn: never;
|
|
35
|
+
identity: undefined;
|
|
36
|
+
generated: undefined;
|
|
37
|
+
}, {}, {
|
|
38
|
+
length: number | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
product: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
41
|
+
name: "product";
|
|
42
|
+
tableName: string;
|
|
43
|
+
dataType: "string";
|
|
44
|
+
columnType: "SQLiteText";
|
|
45
|
+
data: string;
|
|
46
|
+
driverParam: string;
|
|
47
|
+
notNull: true;
|
|
48
|
+
hasDefault: false;
|
|
49
|
+
isPrimaryKey: false;
|
|
50
|
+
isAutoincrement: false;
|
|
51
|
+
hasRuntimeDefault: false;
|
|
52
|
+
enumValues: [string, ...string[]];
|
|
53
|
+
baseColumn: never;
|
|
54
|
+
identity: undefined;
|
|
55
|
+
generated: undefined;
|
|
56
|
+
}, {}, {
|
|
57
|
+
length: number | undefined;
|
|
58
|
+
}>;
|
|
59
|
+
skillId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
60
|
+
name: "skill_id";
|
|
61
|
+
tableName: string;
|
|
62
|
+
dataType: "string";
|
|
63
|
+
columnType: "SQLiteText";
|
|
64
|
+
data: string;
|
|
65
|
+
driverParam: string;
|
|
66
|
+
notNull: true;
|
|
67
|
+
hasDefault: false;
|
|
68
|
+
isPrimaryKey: false;
|
|
69
|
+
isAutoincrement: false;
|
|
70
|
+
hasRuntimeDefault: false;
|
|
71
|
+
enumValues: [string, ...string[]];
|
|
72
|
+
baseColumn: never;
|
|
73
|
+
identity: undefined;
|
|
74
|
+
generated: undefined;
|
|
75
|
+
}, {}, {
|
|
76
|
+
length: number | undefined;
|
|
77
|
+
}>;
|
|
78
|
+
description: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
79
|
+
name: "description";
|
|
80
|
+
tableName: string;
|
|
81
|
+
dataType: "string";
|
|
82
|
+
columnType: "SQLiteText";
|
|
83
|
+
data: string;
|
|
84
|
+
driverParam: string;
|
|
85
|
+
notNull: true;
|
|
86
|
+
hasDefault: false;
|
|
87
|
+
isPrimaryKey: false;
|
|
88
|
+
isAutoincrement: false;
|
|
89
|
+
hasRuntimeDefault: false;
|
|
90
|
+
enumValues: [string, ...string[]];
|
|
91
|
+
baseColumn: never;
|
|
92
|
+
identity: undefined;
|
|
93
|
+
generated: undefined;
|
|
94
|
+
}, {}, {
|
|
95
|
+
length: number | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
source: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
98
|
+
name: "source";
|
|
99
|
+
tableName: string;
|
|
100
|
+
dataType: "string";
|
|
101
|
+
columnType: "SQLiteText";
|
|
102
|
+
data: string;
|
|
103
|
+
driverParam: string;
|
|
104
|
+
notNull: false;
|
|
105
|
+
hasDefault: false;
|
|
106
|
+
isPrimaryKey: false;
|
|
107
|
+
isAutoincrement: false;
|
|
108
|
+
hasRuntimeDefault: false;
|
|
109
|
+
enumValues: [string, ...string[]];
|
|
110
|
+
baseColumn: never;
|
|
111
|
+
identity: undefined;
|
|
112
|
+
generated: undefined;
|
|
113
|
+
}, {}, {
|
|
114
|
+
length: number | undefined;
|
|
115
|
+
}>;
|
|
116
|
+
};
|
|
117
|
+
dialect: 'sqlite';
|
|
118
|
+
}>;
|
|
119
|
+
/** A drizzle-backed read-only MarketplaceStore over the standard table. */
|
|
120
|
+
export declare function createDrizzleMarketplaceStore(db: DrizzleLike, table: ReturnType<typeof marketplaceSkillTable>): MarketplaceStore;
|
|
121
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateMarketplaceCatalog
|
|
3
|
+
} from "../chunk-LTWJYNEC.js";
|
|
4
|
+
|
|
5
|
+
// src/skills/marketplace-drizzle.ts
|
|
6
|
+
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
7
|
+
function marketplaceSkillTable(tableName = "skill_marketplace") {
|
|
8
|
+
return sqliteTable(tableName, {
|
|
9
|
+
id: text("id").primaryKey(),
|
|
10
|
+
// namespaced `product:id`
|
|
11
|
+
product: text("product").notNull(),
|
|
12
|
+
skillId: text("skill_id").notNull(),
|
|
13
|
+
description: text("description").notNull(),
|
|
14
|
+
source: text("source")
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function createDrizzleMarketplaceStore(db, table) {
|
|
18
|
+
return {
|
|
19
|
+
async list() {
|
|
20
|
+
const rows = await db.select().from(table);
|
|
21
|
+
return validateMarketplaceCatalog(rows.map((row) => ({
|
|
22
|
+
id: row.skillId,
|
|
23
|
+
product: row.product,
|
|
24
|
+
description: row.description,
|
|
25
|
+
...row.source !== null && row.source !== void 0 ? { source: row.source } : {}
|
|
26
|
+
})));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
createDrizzleMarketplaceStore,
|
|
32
|
+
marketplaceSkillTable
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=marketplace-drizzle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/skills/marketplace-drizzle.ts"],"sourcesContent":["/**\n * Drizzle/D1 adapter for the cross-agent skill marketplace — the durable\n * store shape shared by every product that publishes or reads a shared\n * catalog (see `./skills` for the entry format and merge contract).\n *\n * Kept on its own subpath so `./skills` stays substrate-free: this is the\n * only marketplace entry that touches the drizzle peer, mirroring\n * `./agent-profiles/drizzle`.\n */\nimport type { MarketplaceSkill, MarketplaceStore } from './marketplace'\nimport { validateMarketplaceCatalog } from './marketplace'\nimport { sqliteTable, text } from 'drizzle-orm/sqlite-core'\n\n/** Any drizzle database instance (structural — the adapter only uses select\n * over the standard table shape). */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype DrizzleLike = any\n\n/** The standard marketplace table: one row per namespaced `product:id`.\n * Create it in a drizzle/D1 database, publish rows via\n * `marketplaceRowsFor`, read via {@link createDrizzleMarketplaceStore}. */\nexport function marketplaceSkillTable(tableName = 'skill_marketplace') {\n return sqliteTable(tableName, {\n id: text('id').primaryKey(), // namespaced `product:id`\n product: text('product').notNull(),\n skillId: text('skill_id').notNull(),\n description: text('description').notNull(),\n source: text('source'),\n })\n}\n\n/** A drizzle-backed read-only MarketplaceStore over the standard table. */\nexport function createDrizzleMarketplaceStore(\n db: DrizzleLike,\n table: ReturnType<typeof marketplaceSkillTable>,\n): MarketplaceStore {\n return {\n async list(): Promise<readonly MarketplaceSkill[]> {\n const rows: Array<{ id: string; product: string; skillId: string; description: string; source: string | null }>\n = await db.select().from(table)\n return validateMarketplaceCatalog(rows.map((row) => ({\n id: row.skillId,\n product: row.product,\n description: row.description,\n ...(row.source !== null && row.source !== undefined ? { source: row.source } : {}),\n })))\n },\n }\n}\n"],"mappings":";;;;;AAWA,SAAS,aAAa,YAAY;AAU3B,SAAS,sBAAsB,YAAY,qBAAqB;AACrE,SAAO,YAAY,WAAW;AAAA,IAC5B,IAAI,KAAK,IAAI,EAAE,WAAW;AAAA;AAAA,IAC1B,SAAS,KAAK,SAAS,EAAE,QAAQ;AAAA,IACjC,SAAS,KAAK,UAAU,EAAE,QAAQ;AAAA,IAClC,aAAa,KAAK,aAAa,EAAE,QAAQ;AAAA,IACzC,QAAQ,KAAK,QAAQ;AAAA,EACvB,CAAC;AACH;AAGO,SAAS,8BACd,IACA,OACkB;AAClB,SAAO;AAAA,IACL,MAAM,OAA6C;AACjD,YAAM,OACF,MAAM,GAAG,OAAO,EAAE,KAAK,KAAK;AAChC,aAAO,2BAA2B,KAAK,IAAI,CAAC,SAAS;AAAA,QACnD,IAAI,IAAI;AAAA,QACR,SAAS,IAAI;AAAA,QACb,aAAa,IAAI;AAAA,QACjB,GAAI,IAAI,WAAW,QAAQ,IAAI,WAAW,SAAY,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;AAAA,MAClF,EAAE,CAAC;AAAA,IACL;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cross-agent skill marketplace — a shared catalog contract on top of the
|
|
3
|
+
* self-mount verb.
|
|
4
|
+
*
|
|
5
|
+
* Every product with the self-improve verb carries its own skill catalog
|
|
6
|
+
* (8–12 skills each, fleet-wide), mounting them through
|
|
7
|
+
* `createMountProfileChangeTool`, which already validates ids, caps mounts
|
|
8
|
+
* and protects the baseline. What the fleet lacks is SHARING: a kicad skill
|
|
9
|
+
* proven on hardware-agent should mount on the legal agent (patent drawings)
|
|
10
|
+
* or the creative agent (product CAD) without a copy in every repo.
|
|
11
|
+
*
|
|
12
|
+
* This module is the marketplace's substrate: the catalog ENTRY format, a
|
|
13
|
+
* read-only store port with the standard adapters, the merge that unions a
|
|
14
|
+
* product's local catalog with shared entries (namespaced `product:id`,
|
|
15
|
+
* local precedence, deduped, capped), and the one-line discovery fragment
|
|
16
|
+
* products surface next to the verb's catalog. Mounting, validation, caps
|
|
17
|
+
* and baseline protection stay exactly where they were proven — in the
|
|
18
|
+
* verb. The marketplace only decides what MAY be discovered and mounted.
|
|
19
|
+
*
|
|
20
|
+
* An entry advertises the skill's identity, not its content: the owning
|
|
21
|
+
* product ships SKILL.md bodies and executable companions in its own repo
|
|
22
|
+
* (and its own profile mounts), so sharing is opt-in per product and the
|
|
23
|
+
* entry is honest about what a foreign mount means — the description and
|
|
24
|
+
* the owning product, nothing hidden.
|
|
25
|
+
*/
|
|
26
|
+
/** A shareable skill as the owning product publishes it. */
|
|
27
|
+
export interface MarketplaceSkill {
|
|
28
|
+
/** Registry-unique skill id WITHIN the owning product (slug: lowercase
|
|
29
|
+
* letters, digits, hyphens). Mounted cross-product as `product:id`. */
|
|
30
|
+
readonly id: string;
|
|
31
|
+
/** The owning product's slug (same alphabet). */
|
|
32
|
+
readonly product: string;
|
|
33
|
+
/** One-line description surfaced in discovery and the verb's catalog. */
|
|
34
|
+
readonly description: string;
|
|
35
|
+
/** Optional pointer to where the skill's content lives (repo path, url).
|
|
36
|
+
* Provenance only — never fetched by this module. */
|
|
37
|
+
readonly source?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Bound on a shared catalog so discovery text stays a prompt-sized line,
|
|
40
|
+
* not a dump. */
|
|
41
|
+
export declare const MAX_MARKETPLACE_ENTRIES = 64;
|
|
42
|
+
/** Validate one marketplace entry — fail loud on a malformed catalog rather
|
|
43
|
+
* than silently mis-mounting it. */
|
|
44
|
+
export declare function validateMarketplaceSkill(entry: MarketplaceSkill): MarketplaceSkill;
|
|
45
|
+
/** The mounted id for a shared skill: `product:id` (namespaced so a foreign
|
|
46
|
+
* skill can never shadow a local one). */
|
|
47
|
+
export declare function namespacedSkillId(entry: {
|
|
48
|
+
product: string;
|
|
49
|
+
id: string;
|
|
50
|
+
}): string;
|
|
51
|
+
/** Split a namespaced id back into `{ product, id }`; a bare id resolves to
|
|
52
|
+
* product `null` (a local skill). */
|
|
53
|
+
export declare function parseNamespacedSkillId(mountedId: string): {
|
|
54
|
+
product: string | null;
|
|
55
|
+
id: string;
|
|
56
|
+
};
|
|
57
|
+
/** Validate a whole shared catalog: entries well-formed, ids unique, size
|
|
58
|
+
* capped. Returns the validated entries in stable (product, id) order. */
|
|
59
|
+
export declare function validateMarketplaceCatalog(entries: readonly MarketplaceSkill[]): readonly MarketplaceSkill[];
|
|
60
|
+
/** A shared-skill entry as it joins a product's local catalog: the
|
|
61
|
+
* namespaced mount id plus the description (annotated with the owner). */
|
|
62
|
+
export interface CatalogSkillEntry {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly description: string;
|
|
65
|
+
}
|
|
66
|
+
/** Union a product's local catalog with shared marketplace entries.
|
|
67
|
+
*
|
|
68
|
+
* Precedence: LOCAL WINS — a namespaced shared id can never collide with a
|
|
69
|
+
* bare local one, and if a product ever adopts its own shared entry, the
|
|
70
|
+
* local description stays authoritative. Output is deduped, sorted, and
|
|
71
|
+
* safe to hand straight to `createMountProfileChangeTool`'s `skills`. */
|
|
72
|
+
export declare function mergeSkillCatalogs(local: readonly CatalogSkillEntry[], shared: readonly MarketplaceSkill[]): readonly CatalogSkillEntry[];
|
|
73
|
+
/** The discovery fragment: one line naming every shared skill a foreign
|
|
74
|
+
* agent can mount, for system prompts and tool descriptions. Returns null
|
|
75
|
+
* when there is nothing to share (no line, no prompt cost). */
|
|
76
|
+
export declare function marketplaceDiscoveryLine(shared: readonly MarketplaceSkill[]): string | null;
|
|
77
|
+
/** Read-only view over a shared catalog. Products source it from their own
|
|
78
|
+
* D1/KV, a platform registry, or a pinned static list — the port stays the
|
|
79
|
+
* same. */
|
|
80
|
+
export interface MarketplaceStore {
|
|
81
|
+
list(): Promise<readonly MarketplaceSkill[]>;
|
|
82
|
+
}
|
|
83
|
+
/** Non-durable default for tests and single-instance products. Validates on
|
|
84
|
+
* construction so a malformed catalog fails at wiring, not at mount. */
|
|
85
|
+
export declare function createInMemoryMarketplaceStore(entries: readonly MarketplaceSkill[]): MarketplaceStore;
|
|
86
|
+
/** Static-list store over a product-pinned catalog (checked into the
|
|
87
|
+
* product repo). Same validation guarantees, no async source. */
|
|
88
|
+
export declare const createStaticMarketplaceStore: typeof createInMemoryMarketplaceStore;
|
|
89
|
+
/** A KV-backed read-only MarketplaceStore (Cloudflare KV or any lookalike).
|
|
90
|
+
* The whole catalog is one JSON value at `prefix:catalog`. */
|
|
91
|
+
export declare function createKVMarketplaceStore(kv: {
|
|
92
|
+
get(key: string): Promise<string | null>;
|
|
93
|
+
}, prefix?: string): MarketplaceStore;
|
|
94
|
+
/** Publish helper for the write side (products/platform own the writer):
|
|
95
|
+
* render validated rows for the standard table (see
|
|
96
|
+
* `./skills/marketplace-drizzle` for the table itself). */
|
|
97
|
+
export interface MarketplaceRow {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly product: string;
|
|
100
|
+
readonly skillId: string;
|
|
101
|
+
readonly description: string;
|
|
102
|
+
readonly source: string | null;
|
|
103
|
+
}
|
|
104
|
+
export declare function marketplaceRowsFor(entries: readonly MarketplaceSkill[]): readonly MarketplaceRow[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/skills-placement/index.ts"],"sourcesContent":["/**\n * Harness-native skill directory resolution — the one place agent-app binds\n * to the platform's authoritative per-harness skill-dir map.\n *\n * `../skills` renders skill CONTENT (parse, tier-filter, `inline`/`mounted`\n * delivery) but deliberately stops short of naming WHICH cwd path a `mounted`\n * skill lands at on a given harness — that mapping is owned by the platform\n * materializer (`@tangle-network/agent-profile-materialize`'s\n * `skillDirForHarness`), not by app-shell. This subpath exists so no product\n * — and no other agent-app module — ever writes a skill path literal\n * (`~/.claude/skills/...`, `.opencode/skills`, ...) of its own; it bridges\n * agent-app's `Harness` taxonomy onto the platform's `HarnessId` and asks the\n * platform for the answer.\n *\n * Requires the OPTIONAL peer `@tangle-network/agent-profile-materialize`.\n * Products that don't install it simply don't import this subpath — every\n * other agent-app skills surface (`../skills`, `ProfileChannels.skillRefs`)\n * works without it, falling back to `inline` delivery.\n */\n\nimport {\n defineInlineResource,\n type AgentProfile,\n type AgentProfileFileMount,\n} from '@tangle-network/agent-interface'\nimport { KNOWN_HARNESSES, type Harness } from '../harness/index'\nimport { skillDirForHarness, type HarnessId } from '@tangle-network/agent-profile-materialize'\nimport { composeSkills, renderInlineSkills, type ComposedSkills, type SkillEntry } from '../skills/index'\n\n/** agent-app `Harness` -> platform `HarnessId`, identity-mapped for exactly\n * the harnesses the platform map covers. Harnesses absent here (`amp`,\n * `factory-droids`, `forge`, `acp`, `cursor`, `cli-base`) resolve to `null` —\n * {@link composeSkillsForHarness} refuses by default rather than silently\n * delivering `inline`; a caller that genuinely needs `inline` on one of\n * these opts in explicitly (`onNoSkillDir: 'inline'`). `cursor`'s adapter\n * supports `resources.skills` bespokely but isn't in the platform map yet;\n * treating it as unbridged is the safe posture until the map covers it,\n * rather than guessing its cwd skill dir here. */\nconst HARNESS_BRIDGE: Partial<Record<Harness, HarnessId>> = {\n opencode: 'opencode',\n 'claude-code': 'claude-code',\n nanoclaw: 'nanoclaw',\n 'kimi-code': 'kimi-code',\n codex: 'codex',\n pi: 'pi',\n prime: 'prime',\n hermes: 'hermes',\n openclaw: 'openclaw',\n}\n\n/** Resolve the cwd-relative skill dir `resources.skills` refs materialize\n * into on `harness` — via the platform's `skillDirForHarness`. `null` when\n * `harness` isn't bridged (see {@link HARNESS_BRIDGE}) or when the platform\n * itself has no cwd skill primitive for it (e.g. `hermes`, user-dir-only). */\nexport function resolveSkillDir(harness: Harness): string | null {\n const bridged = HARNESS_BRIDGE[harness]\n if (!bridged) return null\n return skillDirForHarness(bridged)\n}\n\n/** Filter `harnesses` down to those with no mounted skill dir (deduped,\n * first-seen order preserved) — the set that must fall back to `inline`\n * delivery, or that a caller should warn about before offering \"mounted\"\n * install UX. */\nexport function unsupportedSkillHarnesses(harnesses: Iterable<Harness>): Harness[] {\n const seen = new Set<Harness>()\n const out: Harness[] = []\n for (const harness of harnesses) {\n if (resolveSkillDir(harness) !== null) continue\n if (seen.has(harness)) continue\n seen.add(harness)\n out.push(harness)\n }\n return out\n}\n\n/** Inputs to {@link composeSkillsForHarness}. */\nexport interface ComposeSkillsForHarnessInput {\n skills: SkillEntry[]\n harness: Harness\n tier?: string\n heading?: string\n /**\n * What to do when `harness` has no native skill-mount directory (see\n * {@link resolveSkillDir}). Default `'throw'`: refuse rather than silently\n * inlining every skill BODY into the system prompt. Pass `'inline'` only as\n * a deliberate, auditable opt-in — the caller has decided every skill must\n * reach this harness even though it will be concatenated into the prompt.\n */\n onNoSkillDir?: 'throw' | 'inline'\n}\n\n/** `KNOWN_HARNESSES` filtered to those the platform materializer can mount\n * skill files onto natively (a non-null {@link resolveSkillDir}). Computed\n * fresh rather than cached — the platform map can grow without a release\n * here. Used only to build the actionable error in\n * {@link composeSkillsForHarness}; not exported because it duplicates\n * {@link unsupportedSkillHarnesses}'s complement and callers should reach\n * for that instead when they need the harness list at runtime. */\nfunction nativeSkillMountHarnesses(): Harness[] {\n return KNOWN_HARNESSES.filter((harness) => resolveSkillDir(harness) !== null)\n}\n\n/**\n * Compose {@link ComposedSkills} for `harness`.\n *\n * `mounted` delivery is the standard path: whenever the platform names a cwd\n * skill dir for `harness`, skills ride the typed `resources.skills` channel\n * and the platform materializer writes each one to that directory as a real\n * file — the agent reads it on demand with its own file tools.\n *\n * `inline` delivery — every skill BODY rendered straight into the system\n * prompt — is a NARROW fallback for harnesses with no native skill dir, never\n * a co-equal mode. An `AgentProfile` is a RECIPE that materializes into a\n * sandbox, not a prompt template: skill bodies belong in a file mount, and\n * `prompt.systemPrompt` should carry only what the agent must obey without a\n * tool call (identity, tool-call contract, safety rules, output format) plus\n * a short index of what's mounted. Concatenating skill bodies into the prompt\n * degrades the model toward empty answers once it grows large enough — one\n * product reproduced a 151,882-byte prompt exactly this way, from an\n * unnoticed inline fallback.\n *\n * Because of that, `onNoSkillDir` defaults to `'throw'`: a harness with no\n * skill dir refuses instead of silently inlining. The thrown error names the\n * harness, the skill count, the byte size that would have been inlined, and\n * the harnesses that DO support native mounting. Pass `onNoSkillDir:\n * 'inline'` only as a deliberate, auditable opt-in when every skill genuinely\n * must reach this harness regardless of prompt cost.\n *\n * The one function a product calls instead of hand-checking `resolveSkillDir`\n * and branching between {@link composeSkills}'s two modes itself.\n */\nexport function composeSkillsForHarness(input: ComposeSkillsForHarnessInput): ComposedSkills {\n const { skills, harness, tier, heading, onNoSkillDir = 'throw' } = input\n const skillDir = resolveSkillDir(harness)\n if (skillDir) return composeSkills({ skills, mode: 'mounted', skillDir, tier, heading })\n if (onNoSkillDir === 'inline') return composeSkills({ skills, mode: 'inline', tier, heading })\n\n const relevant = tier ? skills.filter((s) => s.tier === tier) : skills\n const promptSection = renderInlineSkills({ skills, tier, heading })\n const bytes = new TextEncoder().encode(promptSection).byteLength\n const mountable = nativeSkillMountHarnesses()\n throw new Error(\n `composeSkillsForHarness: \"${harness}\" has no native skill-mount directory. ` +\n `Inlining ${relevant.length} skill(s) (${bytes} bytes) into its system prompt would silently ` +\n `convert mounted skills into prompt text — a 151,882-byte prompt shipped this way once, and ` +\n `oversized prompts degrade toward empty answers. Mount the skills instead: pick a harness with ` +\n `native skill support (${mountable.join(', ')}), or pass onNoSkillDir: 'inline' to opt in ` +\n `explicitly and accept the inlined bytes.`,\n )\n}\n\nexport type { ComposedSkills, SkillEntry } from '../skills/index'\n\n/** A file that rides BESIDE a skill entry (kit sources, references, pinned\n * requirements — anything the SKILL.md tells the agent to read or run).\n * `path` is skill-dir-relative (`kit/iso-kit.mjs`, `references/x.md`). */\nexport interface SkillSupportFile {\n path: string\n content: string\n}\n\n/** Inline file mounts placing a skill's support files beside its entry on\n * `harness`: `<skillDir>/<skillId>/<path>`. Empty when the harness has no\n * cwd skill dir — the same posture as `resources.skills` itself (the\n * platform skips skills there too), so nothing orphan lands in a workspace\n * a harness will never read.\n *\n * This is the placement primitive for kit-carrying skills (vendored\n * executable kits, reference trees). A growing program belongs in THESE\n * mounts — never in an env var: runtimeSecrets values are capped (8192\n * chars on the sandbox SDK) and a skill tree outgrew that cap in production\n * once, taking every isolated turn down with a secret-provider refusal. */\nexport function skillSupportFileMounts(\n harness: Harness,\n skillId: string,\n files: readonly SkillSupportFile[],\n): AgentProfileFileMount[] {\n const skillDir = resolveSkillDir(harness)\n if (!skillDir || files.length === 0) return []\n return files.map((file) => ({\n path: `${skillDir}/${skillId}/${file.path}`,\n resource: defineInlineResource(`${skillId}/${file.path}`, file.content),\n }))\n}\n\n/** Attach a skill's support files to `profile` for `harness`, idempotently.\n *\n * Replacement is by EXACT CONTENT under the skill's own subtree (any path\n * ending `/<skillId>/<path>` whose bytes match a support file of THIS call):\n * re-deriving the same profile for another harness replaces this skill's\n * own mounts wherever they previously pointed, while pack-declared files\n * with similar paths stay untouched. With no skill dir the skill's own\n * mounts are dropped (and nothing added) — mirroring how the platform\n * treats the skill entry itself on that harness. */\nexport function withSkillSupportFiles<\n P extends { resources?: AgentProfile['resources'] },\n>(profile: P, harness: Harness, skillId: string, files: readonly SkillSupportFile[]): P {\n const hasSkillDir = resolveSkillDir(harness) !== null\n const desired = skillSupportFileMounts(harness, skillId, files)\n const desiredByPath = new Map(\n desired.map((mount) => [mount.path, mount.resource] as const),\n )\n const byRelPath = new Map(files.map((file) => [file.path, file.content] as const))\n const suffix = `/${skillId}/`\n const relUnderSkill = (path: string): string | null => {\n const at = path.lastIndexOf(suffix)\n return at < 0 ? null : path.slice(at + suffix.length)\n }\n // \"Ours\" = a mount under THIS skill's subtree whose bytes match a support\n // file of this call. It is STALE when the harness has no skill dir (there\n // is nowhere to place it — mirroring how the platform skips the skill\n // entry itself) or when this call would place it at a different path\n // (re-derivation for another harness). Exact-content identity only: a\n // foreign mount at one of our paths keeps the location (pack-declared\n // files win at their declared paths), and a mount already identical to\n // what we would add is kept as-is, so re-deriving for the SAME harness\n // returns the profile unchanged.\n const isStaleOurs = (mount: AgentProfileFileMount): boolean => {\n if (mount.resource.kind !== 'inline') return false\n const rel = relUnderSkill(mount.path)\n if (rel === null || byRelPath.get(rel) !== mount.resource.content) return false\n const desiredHere = desiredByPath.get(mount.path)\n return (\n !hasSkillDir ||\n desiredHere === undefined ||\n desiredHere.kind !== 'inline' ||\n desiredHere.content !== mount.resource.content\n )\n }\n const existing = profile.resources?.files ?? []\n const retained = existing.filter((mount) => !isStaleOurs(mount))\n const taken = new Set(retained.map((mount) => mount.path))\n const additions = desired.filter((mount) => !taken.has(mount.path))\n if (retained.length === existing.length && additions.length === 0) return profile\n return {\n ...profile,\n resources: { ...(profile.resources ?? {}), files: [...retained, ...additions] },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/skills-placement/index.ts"],"sourcesContent":["/**\n * Harness-native skill directory resolution — the one place agent-app binds\n * to the platform's authoritative per-harness skill-dir map.\n *\n * `../skills` renders skill CONTENT (parse, tier-filter, `inline`/`mounted`\n * delivery) but deliberately stops short of naming WHICH cwd path a `mounted`\n * skill lands at on a given harness — that mapping is owned by the platform\n * materializer (`@tangle-network/agent-profile-materialize`'s\n * `skillDirForHarness`), not by app-shell. This subpath exists so no product\n * — and no other agent-app module — ever writes a skill path literal\n * (`~/.claude/skills/...`, `.opencode/skills`, ...) of its own; it bridges\n * agent-app's `Harness` taxonomy onto the platform's `HarnessId` and asks the\n * platform for the answer.\n *\n * Requires the OPTIONAL peer `@tangle-network/agent-profile-materialize`.\n * Products that don't install it simply don't import this subpath — every\n * other agent-app skills surface (`../skills`, `ProfileChannels.skillRefs`)\n * works without it, falling back to `inline` delivery.\n */\n\nimport {\n defineInlineResource,\n type AgentProfile,\n type AgentProfileFileMount,\n} from '@tangle-network/agent-interface'\nimport { KNOWN_HARNESSES, type Harness } from '../harness/index'\nimport { skillDirForHarness, type HarnessId } from '@tangle-network/agent-profile-materialize'\nimport { composeSkills, renderInlineSkills, type ComposedSkills, type SkillEntry } from '../skills/index'\n\n/** agent-app `Harness` -> platform `HarnessId`, identity-mapped for exactly\n * the harnesses the platform map covers. Harnesses absent here (`amp`,\n * `factory-droids`, `forge`, `acp`, `cursor`, `cli-base`) resolve to `null` —\n * {@link composeSkillsForHarness} refuses by default rather than silently\n * delivering `inline`; a caller that genuinely needs `inline` on one of\n * these opts in explicitly (`onNoSkillDir: 'inline'`). `cursor`'s adapter\n * supports `resources.skills` bespokely but isn't in the platform map yet;\n * treating it as unbridged is the safe posture until the map covers it,\n * rather than guessing its cwd skill dir here. */\nconst HARNESS_BRIDGE: Partial<Record<Harness, HarnessId>> = {\n opencode: 'opencode',\n 'claude-code': 'claude-code',\n nanoclaw: 'nanoclaw',\n 'kimi-code': 'kimi-code',\n codex: 'codex',\n pi: 'pi',\n prime: 'prime',\n hermes: 'hermes',\n openclaw: 'openclaw',\n}\n\n/** Resolve the cwd-relative skill dir `resources.skills` refs materialize\n * into on `harness` — via the platform's `skillDirForHarness`. `null` when\n * `harness` isn't bridged (see {@link HARNESS_BRIDGE}) or when the platform\n * itself has no cwd skill primitive for it (e.g. `hermes`, user-dir-only). */\nexport function resolveSkillDir(harness: Harness): string | null {\n const bridged = HARNESS_BRIDGE[harness]\n if (!bridged) return null\n return skillDirForHarness(bridged)\n}\n\n/** Filter `harnesses` down to those with no mounted skill dir (deduped,\n * first-seen order preserved) — the set that must fall back to `inline`\n * delivery, or that a caller should warn about before offering \"mounted\"\n * install UX. */\nexport function unsupportedSkillHarnesses(harnesses: Iterable<Harness>): Harness[] {\n const seen = new Set<Harness>()\n const out: Harness[] = []\n for (const harness of harnesses) {\n if (resolveSkillDir(harness) !== null) continue\n if (seen.has(harness)) continue\n seen.add(harness)\n out.push(harness)\n }\n return out\n}\n\n/** Inputs to {@link composeSkillsForHarness}. */\nexport interface ComposeSkillsForHarnessInput {\n skills: SkillEntry[]\n harness: Harness\n tier?: string\n heading?: string\n /**\n * What to do when `harness` has no native skill-mount directory (see\n * {@link resolveSkillDir}). Default `'throw'`: refuse rather than silently\n * inlining every skill BODY into the system prompt. Pass `'inline'` only as\n * a deliberate, auditable opt-in — the caller has decided every skill must\n * reach this harness even though it will be concatenated into the prompt.\n */\n onNoSkillDir?: 'throw' | 'inline'\n}\n\n/** `KNOWN_HARNESSES` filtered to those the platform materializer can mount\n * skill files onto natively (a non-null {@link resolveSkillDir}). Computed\n * fresh rather than cached — the platform map can grow without a release\n * here. Used only to build the actionable error in\n * {@link composeSkillsForHarness}; not exported because it duplicates\n * {@link unsupportedSkillHarnesses}'s complement and callers should reach\n * for that instead when they need the harness list at runtime. */\nfunction nativeSkillMountHarnesses(): Harness[] {\n return KNOWN_HARNESSES.filter((harness) => resolveSkillDir(harness) !== null)\n}\n\n/**\n * Compose {@link ComposedSkills} for `harness`.\n *\n * `mounted` delivery is the standard path: whenever the platform names a cwd\n * skill dir for `harness`, skills ride the typed `resources.skills` channel\n * and the platform materializer writes each one to that directory as a real\n * file — the agent reads it on demand with its own file tools.\n *\n * `inline` delivery — every skill BODY rendered straight into the system\n * prompt — is a NARROW fallback for harnesses with no native skill dir, never\n * a co-equal mode. An `AgentProfile` is a RECIPE that materializes into a\n * sandbox, not a prompt template: skill bodies belong in a file mount, and\n * `prompt.systemPrompt` should carry only what the agent must obey without a\n * tool call (identity, tool-call contract, safety rules, output format) plus\n * a short index of what's mounted. Concatenating skill bodies into the prompt\n * degrades the model toward empty answers once it grows large enough — one\n * product reproduced a 151,882-byte prompt exactly this way, from an\n * unnoticed inline fallback.\n *\n * Because of that, `onNoSkillDir` defaults to `'throw'`: a harness with no\n * skill dir refuses instead of silently inlining. The thrown error names the\n * harness, the skill count, the byte size that would have been inlined, and\n * the harnesses that DO support native mounting. Pass `onNoSkillDir:\n * 'inline'` only as a deliberate, auditable opt-in when every skill genuinely\n * must reach this harness regardless of prompt cost.\n *\n * The one function a product calls instead of hand-checking `resolveSkillDir`\n * and branching between {@link composeSkills}'s two modes itself.\n */\nexport function composeSkillsForHarness(input: ComposeSkillsForHarnessInput): ComposedSkills {\n const { skills, harness, tier, heading, onNoSkillDir = 'throw' } = input\n const skillDir = resolveSkillDir(harness)\n if (skillDir) return composeSkills({ skills, mode: 'mounted', skillDir, tier, heading })\n if (onNoSkillDir === 'inline') return composeSkills({ skills, mode: 'inline', tier, heading })\n\n const relevant = tier ? skills.filter((s) => s.tier === tier) : skills\n const promptSection = renderInlineSkills({ skills, tier, heading })\n const bytes = new TextEncoder().encode(promptSection).byteLength\n const mountable = nativeSkillMountHarnesses()\n throw new Error(\n `composeSkillsForHarness: \"${harness}\" has no native skill-mount directory. ` +\n `Inlining ${relevant.length} skill(s) (${bytes} bytes) into its system prompt would silently ` +\n `convert mounted skills into prompt text — a 151,882-byte prompt shipped this way once, and ` +\n `oversized prompts degrade toward empty answers. Mount the skills instead: pick a harness with ` +\n `native skill support (${mountable.join(', ')}), or pass onNoSkillDir: 'inline' to opt in ` +\n `explicitly and accept the inlined bytes.`,\n )\n}\n\nexport type { ComposedSkills, SkillEntry } from '../skills/index'\n\n/** A file that rides BESIDE a skill entry (kit sources, references, pinned\n * requirements — anything the SKILL.md tells the agent to read or run).\n * `path` is skill-dir-relative (`kit/iso-kit.mjs`, `references/x.md`). */\nexport interface SkillSupportFile {\n path: string\n content: string\n}\n\n/** Inline file mounts placing a skill's support files beside its entry on\n * `harness`: `<skillDir>/<skillId>/<path>`. Empty when the harness has no\n * cwd skill dir — the same posture as `resources.skills` itself (the\n * platform skips skills there too), so nothing orphan lands in a workspace\n * a harness will never read.\n *\n * This is the placement primitive for kit-carrying skills (vendored\n * executable kits, reference trees). A growing program belongs in THESE\n * mounts — never in an env var: runtimeSecrets values are capped (8192\n * chars on the sandbox SDK) and a skill tree outgrew that cap in production\n * once, taking every isolated turn down with a secret-provider refusal. */\nexport function skillSupportFileMounts(\n harness: Harness,\n skillId: string,\n files: readonly SkillSupportFile[],\n): AgentProfileFileMount[] {\n const skillDir = resolveSkillDir(harness)\n if (!skillDir || files.length === 0) return []\n return files.map((file) => ({\n path: `${skillDir}/${skillId}/${file.path}`,\n resource: defineInlineResource(`${skillId}/${file.path}`, file.content),\n }))\n}\n\n/** Attach a skill's support files to `profile` for `harness`, idempotently.\n *\n * Replacement is by EXACT CONTENT under the skill's own subtree (any path\n * ending `/<skillId>/<path>` whose bytes match a support file of THIS call):\n * re-deriving the same profile for another harness replaces this skill's\n * own mounts wherever they previously pointed, while pack-declared files\n * with similar paths stay untouched. With no skill dir the skill's own\n * mounts are dropped (and nothing added) — mirroring how the platform\n * treats the skill entry itself on that harness. */\nexport function withSkillSupportFiles<\n P extends { resources?: AgentProfile['resources'] },\n>(profile: P, harness: Harness, skillId: string, files: readonly SkillSupportFile[]): P {\n const hasSkillDir = resolveSkillDir(harness) !== null\n const desired = skillSupportFileMounts(harness, skillId, files)\n const desiredByPath = new Map(\n desired.map((mount) => [mount.path, mount.resource] as const),\n )\n const byRelPath = new Map(files.map((file) => [file.path, file.content] as const))\n const suffix = `/${skillId}/`\n const relUnderSkill = (path: string): string | null => {\n const at = path.lastIndexOf(suffix)\n return at < 0 ? null : path.slice(at + suffix.length)\n }\n // \"Ours\" = a mount under THIS skill's subtree whose bytes match a support\n // file of this call. It is STALE when the harness has no skill dir (there\n // is nowhere to place it — mirroring how the platform skips the skill\n // entry itself) or when this call would place it at a different path\n // (re-derivation for another harness). Exact-content identity only: a\n // foreign mount at one of our paths keeps the location (pack-declared\n // files win at their declared paths), and a mount already identical to\n // what we would add is kept as-is, so re-deriving for the SAME harness\n // returns the profile unchanged.\n const isStaleOurs = (mount: AgentProfileFileMount): boolean => {\n if (mount.resource.kind !== 'inline') return false\n const rel = relUnderSkill(mount.path)\n if (rel === null || byRelPath.get(rel) !== mount.resource.content) return false\n const desiredHere = desiredByPath.get(mount.path)\n return (\n !hasSkillDir ||\n desiredHere === undefined ||\n desiredHere.kind !== 'inline' ||\n desiredHere.content !== mount.resource.content\n )\n }\n const existing = profile.resources?.files ?? []\n const retained = existing.filter((mount) => !isStaleOurs(mount))\n const taken = new Set(retained.map((mount) => mount.path))\n const additions = desired.filter((mount) => !taken.has(mount.path))\n if (retained.length === existing.length && additions.length === 0) return profile\n return {\n ...profile,\n resources: { ...(profile.resources ?? {}), files: [...retained, ...additions] },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The budget-derived delegated mint — the platform's reservation contract as
|
|
3
|
+
* one function.
|
|
4
|
+
*
|
|
5
|
+
* The platform RESERVES a delegated child key's budget against its parent's
|
|
6
|
+
* REMAINING budget: an ask above remaining is a hard refusal (no amounts in
|
|
7
|
+
* the response), which is how every delegated mint in a fleet product died
|
|
8
|
+
* until the derivation landed (hardware-agent #165, 2026-10-09: a flat $25
|
|
9
|
+
* default asked of a $5-remaining parent refused 100% of mints). Products
|
|
10
|
+
* that mint delegated keys must derive the ask from what the platform
|
|
11
|
+
* actually guarantees:
|
|
12
|
+
*
|
|
13
|
+
* - the parent's remaining budget is UNKNOWN → the configured ask stands
|
|
14
|
+
* (the platform isn't reporting a ceiling; nothing better is knowable);
|
|
15
|
+
* - remaining ≤ 0 → refuse with a correctable top-up error, before any
|
|
16
|
+
* mint is attempted;
|
|
17
|
+
* - otherwise → ask min(configured, remaining) floored to whole cents,
|
|
18
|
+
* never below the platform's minimum child budget (the floor keeps the
|
|
19
|
+
* mint VALID — a below-minimum ask is refused by the key API).
|
|
20
|
+
*
|
|
21
|
+
* Note the boundary honestly: with remaining between $0.01 and the minimum,
|
|
22
|
+
* the derived ask (the floor) still exceeds remaining and the platform will
|
|
23
|
+
* refuse it — that case is indistinguishable from exhausted for the caller
|
|
24
|
+
* and surfaces through the same error path with the numbers attached.
|
|
25
|
+
*/
|
|
26
|
+
/** The platform's minimum child-key budget. Asks below it are refused by the
|
|
27
|
+
* key API, so the derivation never produces one. */
|
|
28
|
+
export declare const MIN_CHILD_KEY_BUDGET_USD = 0.5;
|
|
29
|
+
/** The source key is exhausted — the only refusal this contract throws.
|
|
30
|
+
* Correctable by design: both numbers the decision used are on the error. */
|
|
31
|
+
export declare class ParentBudgetExhaustedError extends Error {
|
|
32
|
+
readonly requestedUsd: number;
|
|
33
|
+
readonly parentRemainingUsd: number;
|
|
34
|
+
constructor(requestedUsd: number, parentRemainingUsd: number);
|
|
35
|
+
}
|
|
36
|
+
export interface DeriveChildKeyBudgetInput {
|
|
37
|
+
/** The configured/default ask (the product's mint budget). Must be a
|
|
38
|
+
* finite positive number. */
|
|
39
|
+
readonly requestedUsd: number;
|
|
40
|
+
/** The parent's REMAINING budget as the platform's verify/key info
|
|
41
|
+
* reports it. `null`/`undefined`/non-finite = unknown (not reported). */
|
|
42
|
+
readonly parentRemainingUsd?: number | null;
|
|
43
|
+
/** Minimum child budget override. Default {@link MIN_CHILD_KEY_BUDGET_USD}. */
|
|
44
|
+
readonly minChildUsd?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface DerivedChildKeyBudget {
|
|
47
|
+
/** The ask the mint should make — never above a known remaining, never
|
|
48
|
+
* below the platform minimum. */
|
|
49
|
+
readonly budgetUsd: number;
|
|
50
|
+
/** True when the ask was lowered (or floored) because the parent holds
|
|
51
|
+
* less than the configured default. */
|
|
52
|
+
readonly derivedFromParent: boolean;
|
|
53
|
+
/** True when the platform reports no ceiling and the configured ask stood. */
|
|
54
|
+
readonly parentRemainingUnknown: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Derive the delegated child-key budget from the parent's remaining budget.
|
|
57
|
+
* Throws {@link ParentBudgetExhaustedError} when the parent is exhausted,
|
|
58
|
+
* before any mint is attempted. */
|
|
59
|
+
export declare function deriveChildKeyBudgetUsd(input: DeriveChildKeyBudgetInput): DerivedChildKeyBudget;
|
package/dist/spend/index.d.ts
CHANGED
|
@@ -36,4 +36,5 @@ export { DEFAULT_CEILING_TOLERANCE_MS, computeExpectedCeiling, type ComputeExpec
|
|
|
36
36
|
export { chargeNanoUsd, isCharge, parseSandboxGroupKey, parseSettlementReference, settlementSandboxId, } from './reference';
|
|
37
37
|
export { reconcileSpend, type BoxRateResolver, type ObservedBalance, type ReconcileSpendOptions, type VelocityOptions, } from './reconcile';
|
|
38
38
|
export { ComputeBudgetExceededError, assertComputeBudget, createSandboxSpendHooks, type ComputeBudget, type ComputeBudgetRefusal, type SandboxSpendHooksOptions, type SandboxSpendSeam, type SpendProvisionObservation, } from './budget';
|
|
39
|
+
export { MIN_CHILD_KEY_BUDGET_USD, ParentBudgetExhaustedError, deriveChildKeyBudgetUsd, type DeriveChildKeyBudgetInput, type DerivedChildKeyBudget, } from './delegate-budget';
|
|
39
40
|
export { formatSpendReport, spendReportToJson } from './report';
|
package/dist/spend/index.js
CHANGED
|
@@ -186,10 +186,52 @@ function createSandboxSpendHooks(options) {
|
|
|
186
186
|
}
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
// src/spend/delegate-budget.ts
|
|
191
|
+
var MIN_CHILD_KEY_BUDGET_USD = 0.5;
|
|
192
|
+
var ParentBudgetExhaustedError = class extends Error {
|
|
193
|
+
requestedUsd;
|
|
194
|
+
parentRemainingUsd;
|
|
195
|
+
constructor(requestedUsd, parentRemainingUsd) {
|
|
196
|
+
super(
|
|
197
|
+
`workspace source key has no budget remaining ($${parentRemainingUsd.toFixed(2)}) \u2014 top up or raise its cap before sandbox turns (the mint would have asked $${requestedUsd.toFixed(2)})`
|
|
198
|
+
);
|
|
199
|
+
this.name = "ParentBudgetExhaustedError";
|
|
200
|
+
this.requestedUsd = requestedUsd;
|
|
201
|
+
this.parentRemainingUsd = parentRemainingUsd;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function deriveChildKeyBudgetUsd(input) {
|
|
205
|
+
const minChildUsd = input.minChildUsd ?? MIN_CHILD_KEY_BUDGET_USD;
|
|
206
|
+
if (!Number.isFinite(input.requestedUsd) || input.requestedUsd <= 0) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`deriveChildKeyBudgetUsd: requestedUsd must be a finite positive USD amount (got ${input.requestedUsd})`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!Number.isFinite(minChildUsd) || minChildUsd <= 0) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`deriveChildKeyBudgetUsd: minChildUsd must be a finite positive USD amount (got ${minChildUsd})`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const ceiling = input.parentRemainingUsd;
|
|
217
|
+
const known = typeof ceiling === "number" && Number.isFinite(ceiling);
|
|
218
|
+
if (!known) {
|
|
219
|
+
return { budgetUsd: input.requestedUsd, derivedFromParent: false, parentRemainingUnknown: true };
|
|
220
|
+
}
|
|
221
|
+
if (ceiling <= 0) throw new ParentBudgetExhaustedError(input.requestedUsd, ceiling);
|
|
222
|
+
const budgetUsd = Math.max(minChildUsd, Math.min(input.requestedUsd, Math.floor(ceiling * 100) / 100));
|
|
223
|
+
return {
|
|
224
|
+
budgetUsd,
|
|
225
|
+
derivedFromParent: budgetUsd < input.requestedUsd || ceiling < minChildUsd,
|
|
226
|
+
parentRemainingUnknown: false
|
|
227
|
+
};
|
|
228
|
+
}
|
|
189
229
|
export {
|
|
190
230
|
ComputeBudgetExceededError,
|
|
191
231
|
DEFAULT_CEILING_TOLERANCE_MS,
|
|
192
232
|
DEFAULT_EXPECTATION_GRACE_MS,
|
|
233
|
+
MIN_CHILD_KEY_BUDGET_USD,
|
|
234
|
+
ParentBudgetExhaustedError,
|
|
193
235
|
SPEND_CHECKS,
|
|
194
236
|
assertComputeBudget,
|
|
195
237
|
assertSpendWindow,
|
|
@@ -201,6 +243,7 @@ export {
|
|
|
201
243
|
createSandboxSpendHooks,
|
|
202
244
|
createSpendLedger,
|
|
203
245
|
decideBoxOwnership,
|
|
246
|
+
deriveChildKeyBudgetUsd,
|
|
204
247
|
foldSpendBoxRecord,
|
|
205
248
|
formatSpendReport,
|
|
206
249
|
isCharge,
|
package/dist/spend/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/spend/store.ts","../../src/spend/budget.ts"],"sourcesContent":["import type { SpendBoxPatch, SpendBoxRecord, SpendWindow } from './types'\n\n/**\n * Persistence seam for the expectation ledger — the product implements it over\n * its own tables.\n *\n * Deliberately NOT compare-and-set, unlike `MissionStorePort`. A mission has one\n * serialized owner and a lost write corrupts a state machine; a box record is a\n * MONOTONIC FOLD (activity takes a max, a detached-run id joins or leaves a set,\n * delete is set-once) so concurrent writers converge no matter what order they\n * land in. The worst a lost race can do here is leave `lastActivityAt` behind\n * the truth — which makes the derived ceiling TIGHTER, so the failure mode is a\n * false alarm a human dismisses, never a missed charge. That asymmetry is the\n * whole reason the fold is shaped this way.\n *\n * `update` returns null when the row does not exist, never a throw.\n */\nexport interface SpendLedgerStorePort {\n load(sandboxId: string): Promise<SpendBoxRecord | null>\n /** `extras` are the opaque product-column values — write them in the SAME\n * statement as the record, or ignore them if the table has no extra columns. */\n insert(record: SpendBoxRecord, extras?: Record<string, unknown>): Promise<SpendBoxRecord>\n update(sandboxId: string, patch: SpendBoxPatch): Promise<SpendBoxRecord | null>\n /**\n * OPTIONAL — every box that could have been live during the window.\n *\n * This is the one capability that lets a reconciliation answer \"were we NOT\n * billed for something we DID ask for\". Without it the pass is driven entirely\n * by settlement rows, so a feed that returns nothing fires nothing and reads\n * as a clean bill. Omitting it is safe and additive: the pass reports\n * `expectation.declared: false` and refuses to certify a bill it could not\n * check (see {@link SpendReport.coverage}).\n *\n * **It may over-return.** The reconciler re-derives liveness itself with\n * `boxLivenessInWindow`, so a store that returns every row it has is correct,\n * merely slower. That is deliberate: the definition of \"live\" must live in one\n * place, not once per product's SQL. The intended predicate is the coarse one\n * a WHERE clause can express —\n *\n * ```sql\n * WHERE created_at <= :endAt AND (deleted_at IS NULL OR deleted_at >= :startAt)\n * ```\n *\n * — and the exact answer (idle timeout, max lifetime, stop, open detached run)\n * is the reconciler's.\n */\n listLiveBetween?(window: SpendWindow): Promise<readonly SpendBoxRecord[]>\n}\n\n/**\n * Apply one fold step. Exported so a SQL implementation and an in-memory one\n * reach the same record, and so a product can unit-test its own store against\n * the canonical answer.\n *\n * The two rules worth stating out loud:\n *\n * - `observedActivityAt` only ever moves `lastActivityAt` FORWARD. A replayed\n * or out-of-order event cannot rewind the ceiling.\n * - activity later than a recorded `stoppedAt` CLEARS the stop. A box that\n * worked after the product thought it stopped is running again, and keeping\n * the stale stop would make the ceiling too tight — inventing an over-ceiling\n * finding out of the product's own bookkeeping rather than the platform's.\n */\nexport function foldSpendBoxRecord(record: SpendBoxRecord, patch: SpendBoxPatch): SpendBoxRecord {\n let lastActivityAt = record.lastActivityAt\n let stoppedAt = record.stoppedAt\n let openDetachedRunIds = record.openDetachedRunIds\n\n if (patch.observedActivityAt !== undefined) {\n lastActivityAt = Math.max(lastActivityAt, patch.observedActivityAt)\n if (stoppedAt !== null && patch.observedActivityAt > stoppedAt) stoppedAt = null\n }\n if (patch.openDetachedRunAdd !== undefined && !openDetachedRunIds.includes(patch.openDetachedRunAdd)) {\n openDetachedRunIds = [...openDetachedRunIds, patch.openDetachedRunAdd]\n }\n if (patch.openDetachedRunRemove !== undefined) {\n openDetachedRunIds = openDetachedRunIds.filter((id) => id !== patch.openDetachedRunRemove)\n }\n if (patch.stoppedAt !== undefined) {\n // Latest-wins, but never behind observed activity: a stop we are told about\n // that predates work we watched is not the stop that closed this box.\n stoppedAt = patch.stoppedAt >= lastActivityAt ? patch.stoppedAt : stoppedAt\n }\n\n return {\n ...record,\n lastActivityAt,\n stoppedAt,\n openDetachedRunIds,\n // Set-once: a deleted sandbox id never comes back, so a second observation\n // is a duplicate delivery, not a second deletion.\n deletedAt: record.deletedAt ?? patch.deletedAt ?? null,\n }\n}\n\n/** An in-memory store that also lets a test inspect and force state. */\nexport interface InMemorySpendLedgerStore extends SpendLedgerStorePort {\n /** Every record, insertion order. */\n records(): SpendBoxRecord[]\n /** Unguarded direct write — simulates a crash-shaped or platform-seeded row. */\n put(record: SpendBoxRecord): void\n}\n\n/** Create an in-memory expectation ledger. Production writers use the same port. */\nexport function createInMemorySpendLedgerStore(): InMemorySpendLedgerStore {\n const rows = new Map<string, SpendBoxRecord>()\n return {\n async load(sandboxId) {\n const row = rows.get(sandboxId)\n return row ? structuredClone(row) : null\n },\n async insert(record) {\n const stored = structuredClone(record)\n rows.set(record.sandboxId, stored)\n return structuredClone(stored)\n },\n async update(sandboxId, patch) {\n const current = rows.get(sandboxId)\n if (!current) return null\n const next = foldSpendBoxRecord(current, patch)\n rows.set(sandboxId, next)\n return structuredClone(next)\n },\n async listLiveBetween(window) {\n // Deliberately the COARSE predicate a product's SQL can express, not the\n // exact one: the idle-timeout / max-lifetime / detached-run derivation is\n // the reconciler's, and this over-returning is what proves the reconciler\n // re-filters rather than trusting whatever a store hands it.\n return [...rows.values()]\n .filter(\n (row) =>\n row.createdAt <= window.endAt && (row.deletedAt === null || row.deletedAt >= window.startAt),\n )\n .map((row) => structuredClone(row))\n },\n records() {\n return [...rows.values()].map((row) => structuredClone(row))\n },\n put(record) {\n rows.set(record.sandboxId, structuredClone(record))\n },\n }\n}\n\n/** What the product tells the ledger when it first sees a box. */\nexport interface ObserveSandboxInput {\n readonly sandboxId: string\n readonly workspaceId: string\n /** The idle timeout the product asked the platform for, seconds. */\n readonly idleTimeoutSeconds: number\n /** The max lifetime the product asked for, seconds, when it asked for one. */\n readonly maxLifetimeSeconds?: number | null\n /** Defaults to the ledger's clock. */\n readonly at?: number\n}\n\nexport interface SpendLedgerOptions {\n readonly store: SpendLedgerStorePort\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n /** Product columns written verbatim on every insert. */\n readonly extras?: Record<string, unknown>\n}\n\n/**\n * The recording half of spend verification: the product's own account of what\n * it asked the platform for.\n *\n * Every method is best-effort from the caller's point of view — a product wires\n * these into paths that must not fail because bookkeeping failed. They still\n * reject on a store error rather than swallowing it, so a caller that wants\n * fire-and-forget says so at the call site (`/sandbox`'s hook does).\n */\nexport interface SpendLedger {\n /**\n * Record that a box exists and is billable from now. Inserts on first sight,\n * and otherwise records activity — reuse and resume are both \"the platform is\n * charging for this box again\", and the record's own existence is what\n * distinguishes them, so no caller has to know which happened.\n */\n observeSandbox(input: ObserveSandboxInput): Promise<SpendBoxRecord>\n /** Record that the product saw this box do work. */\n recordActivity(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /**\n * Record that the product handed the platform work it will NOT watch finish.\n * Until the matching end is recorded, this box's ceiling cannot rest on\n * observed activity — see `computeExpectedCeiling`.\n */\n recordDetachedRunStarted(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that a detached run was confirmed finished. */\n recordDetachedRunEnded(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box stopped. */\n recordStopped(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box was deleted. */\n recordDeleted(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n}\n\n/** Create the recording half over a product-supplied store. */\nexport function createSpendLedger(options: SpendLedgerOptions): SpendLedger {\n const { store } = options\n const clock = options.now ?? Date.now\n\n return {\n async observeSandbox(input) {\n const at = input.at ?? clock()\n const existing = await store.load(input.sandboxId)\n if (existing) {\n const updated = await store.update(input.sandboxId, { observedActivityAt: at })\n return updated ?? existing\n }\n return await store.insert(\n {\n sandboxId: input.sandboxId,\n workspaceId: input.workspaceId,\n createdAt: at,\n idleTimeoutSeconds: input.idleTimeoutSeconds,\n maxLifetimeSeconds: input.maxLifetimeSeconds ?? null,\n lastActivityAt: at,\n openDetachedRunIds: [],\n stoppedAt: null,\n deletedAt: null,\n },\n options.extras,\n )\n },\n async recordActivity(sandboxId, at) {\n return await store.update(sandboxId, { observedActivityAt: at ?? clock() })\n },\n async recordDetachedRunStarted(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunAdd: runId,\n })\n },\n async recordDetachedRunEnded(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunRemove: runId,\n })\n },\n async recordStopped(sandboxId, at) {\n return await store.update(sandboxId, { stoppedAt: at ?? clock() })\n },\n async recordDeleted(sandboxId, at) {\n return await store.update(sandboxId, { deletedAt: at ?? clock() })\n },\n }\n}\n","import type { SpendLedger } from './store'\n\n/** Why provisioning was refused, with every number the decision used. */\nexport interface ComputeBudgetRefusal {\n readonly workspaceId: string\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for this workspace, unsigned nanodollars. */\n readonly settledNanoUsd: number\n /** How far past the cap it already is. */\n readonly overageNanoUsd: number\n readonly at: number\n}\n\n/**\n * Provisioning refused because the workspace is already past its compute cap.\n *\n * Correctable by design: every number the decision used is on the error, so a\n * product can render \"this workspace has spent $X of its $Y compute budget\" and\n * an operator can raise the cap or investigate without reading logs.\n *\n * This is the failure mode the module exists to produce. A platform billing\n * defect that used to end in a silent negative balance now ends in provisioning\n * stopping and something loud happening instead.\n */\nexport class ComputeBudgetExceededError extends Error {\n readonly workspaceId: string\n readonly limitNanoUsd: number\n readonly settledNanoUsd: number\n readonly overageNanoUsd: number\n\n constructor(refusal: ComputeBudgetRefusal) {\n super(\n `Compute budget exceeded for workspace ${refusal.workspaceId}: ` +\n `$${(refusal.settledNanoUsd / 1_000_000_000).toFixed(2)} settled against a cap of ` +\n `$${(refusal.limitNanoUsd / 1_000_000_000).toFixed(2)} ` +\n `(over by $${(refusal.overageNanoUsd / 1_000_000_000).toFixed(2)}). ` +\n 'No sandbox was provisioned. Raise the cap or reconcile the spend before retrying.',\n )\n this.name = 'ComputeBudgetExceededError'\n this.workspaceId = refusal.workspaceId\n this.limitNanoUsd = refusal.limitNanoUsd\n this.settledNanoUsd = refusal.settledNanoUsd\n this.overageNanoUsd = refusal.overageNanoUsd\n }\n}\n\n/**\n * A per-workspace cap on sandbox compute.\n *\n * `/billing`'s budget primitive caps MODEL keys, and it works because the\n * platform enforces the cap at the key it minted. Sandbox compute has no such\n * key: a box bills the shared company wallet, so nothing upstream refuses. This\n * carries the same shape to the one place a consumer can still act — the moment\n * before it asks for another box.\n *\n * `settledNanoUsd` is a callback rather than a number because the authority is\n * the platform ledger, not this package: the product reads the same rows it\n * hands the reconciler. Cache it if the read is expensive; a cap is a\n * coarse-grained control and a slightly stale total still refuses.\n */\nexport interface ComputeBudget {\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for the workspace, unsigned nanodollars. */\n readonly settledNanoUsd: (workspaceId: string) => Promise<number> | number\n /**\n * Called on every refusal, before the error is thrown. This is the alert\n * seam: a refusal nobody hears is a product that silently stopped working.\n */\n readonly onRefusal?: (refusal: ComputeBudgetRefusal) => void\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n}\n\n/**\n * Throw {@link ComputeBudgetExceededError} when the workspace is already past\n * its cap. Returns normally — and reads nothing — when no budget is configured.\n *\n * Deliberately a pre-check against spend ALREADY SETTLED, not a reservation\n * against spend about to happen: settlement lags provisioning by design (the\n * platform's durable settlement queue), so there is no instant at which a\n * consumer could hold an accurate running total. The cap therefore overshoots by\n * at most the unsettled tail, which is bounded by the box's own idle timeout.\n * A cap that refuses one box late is worth far more than one that cannot be\n * implemented honestly.\n */\nexport async function assertComputeBudget(\n budget: ComputeBudget | undefined,\n workspaceId: string,\n): Promise<void> {\n if (!budget) return\n const settledNanoUsd = await budget.settledNanoUsd(workspaceId)\n if (settledNanoUsd < budget.limitNanoUsd) return\n\n const refusal: ComputeBudgetRefusal = {\n workspaceId,\n limitNanoUsd: budget.limitNanoUsd,\n settledNanoUsd,\n overageNanoUsd: settledNanoUsd - budget.limitNanoUsd,\n at: (budget.now ?? Date.now)(),\n }\n budget.onRefusal?.(refusal)\n throw new ComputeBudgetExceededError(refusal)\n}\n\n// ── the /sandbox seam ─────────────────────────────────────────────────────────\n\n/**\n * What `/sandbox` reports once a box is provisioned, reused or resumed.\n *\n * Structurally identical to `SandboxProvisionedObservation` in `/sandbox`, and\n * deliberately re-declared rather than imported: `/spend` composes `/sandbox`,\n * so a type import in the other direction would invert the dependency. The two\n * are pinned together by a compile-time assignment in this module's tests.\n */\nexport interface SpendProvisionObservation {\n readonly workspaceId: string\n readonly userId?: string\n readonly sandboxId: string\n readonly boxKey?: string | undefined\n readonly idleTimeoutSeconds: number\n readonly maxLifetimeSeconds?: number | undefined\n readonly at: number\n}\n\n/**\n * The optional seam `EnsureWorkspaceSandboxOptions.spend` and the turn\n * primitives' `spend` option both accept. One object, wired in both places.\n */\nexport interface SandboxSpendSeam {\n beforeProvision?(input: { workspaceId: string; userId?: string }): Promise<void> | void\n onProvisioned?(observation: SpendProvisionObservation): Promise<void> | void\n /** Synchronous by contract — it sits on the turn path. See `createSandboxSpendHooks`. */\n onActivity?(input: { sandboxId: string; at: number }): void\n}\n\nexport interface SandboxSpendHooksOptions {\n /** Records box lifecycle. Omit to run the budget guard alone. */\n readonly ledger?: SpendLedger\n /** Refuses provisioning past a cap. Omit to record alone. */\n readonly budget?: ComputeBudget\n /**\n * Called when RECORDING fails. Recording is best-effort — a bookkeeping\n * failure must never take down the provisioning it is bookkeeping — so this\n * is the only place such a failure is visible. A refusal is NOT routed here;\n * refusals throw, by design.\n */\n readonly onError?: (error: unknown) => void\n}\n\n/**\n * Build the object to hand `ensureWorkspaceSandbox`'s `spend` option.\n *\n * Wiring it is the entire adoption cost: one field, and the product's boxes are\n * both budget-capped and recorded.\n */\nexport function createSandboxSpendHooks(options: SandboxSpendHooksOptions): SandboxSpendSeam {\n const { ledger, budget, onError } = options\n return {\n async beforeProvision(input) {\n await assertComputeBudget(budget, input.workspaceId)\n },\n async onProvisioned(observation) {\n if (!ledger) return\n try {\n await ledger.observeSandbox({\n sandboxId: observation.sandboxId,\n workspaceId: observation.workspaceId,\n idleTimeoutSeconds: observation.idleTimeoutSeconds,\n maxLifetimeSeconds: observation.maxLifetimeSeconds ?? null,\n at: observation.at,\n })\n } catch (err) {\n onError?.(err)\n }\n },\n onActivity(input) {\n if (!ledger) return\n // The turn path calls this synchronously and does not await it, so the\n // promise is settled here rather than escaping as an unhandled rejection.\n // Recording activity is a monotonic max, so a write that lands late — or\n // out of order against another turn's — still converges.\n void ledger.recordActivity(input.sandboxId, input.at).catch((err: unknown) => onError?.(err))\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/spend/store.ts","../../src/spend/budget.ts","../../src/spend/delegate-budget.ts"],"sourcesContent":["import type { SpendBoxPatch, SpendBoxRecord, SpendWindow } from './types'\n\n/**\n * Persistence seam for the expectation ledger — the product implements it over\n * its own tables.\n *\n * Deliberately NOT compare-and-set, unlike `MissionStorePort`. A mission has one\n * serialized owner and a lost write corrupts a state machine; a box record is a\n * MONOTONIC FOLD (activity takes a max, a detached-run id joins or leaves a set,\n * delete is set-once) so concurrent writers converge no matter what order they\n * land in. The worst a lost race can do here is leave `lastActivityAt` behind\n * the truth — which makes the derived ceiling TIGHTER, so the failure mode is a\n * false alarm a human dismisses, never a missed charge. That asymmetry is the\n * whole reason the fold is shaped this way.\n *\n * `update` returns null when the row does not exist, never a throw.\n */\nexport interface SpendLedgerStorePort {\n load(sandboxId: string): Promise<SpendBoxRecord | null>\n /** `extras` are the opaque product-column values — write them in the SAME\n * statement as the record, or ignore them if the table has no extra columns. */\n insert(record: SpendBoxRecord, extras?: Record<string, unknown>): Promise<SpendBoxRecord>\n update(sandboxId: string, patch: SpendBoxPatch): Promise<SpendBoxRecord | null>\n /**\n * OPTIONAL — every box that could have been live during the window.\n *\n * This is the one capability that lets a reconciliation answer \"were we NOT\n * billed for something we DID ask for\". Without it the pass is driven entirely\n * by settlement rows, so a feed that returns nothing fires nothing and reads\n * as a clean bill. Omitting it is safe and additive: the pass reports\n * `expectation.declared: false` and refuses to certify a bill it could not\n * check (see {@link SpendReport.coverage}).\n *\n * **It may over-return.** The reconciler re-derives liveness itself with\n * `boxLivenessInWindow`, so a store that returns every row it has is correct,\n * merely slower. That is deliberate: the definition of \"live\" must live in one\n * place, not once per product's SQL. The intended predicate is the coarse one\n * a WHERE clause can express —\n *\n * ```sql\n * WHERE created_at <= :endAt AND (deleted_at IS NULL OR deleted_at >= :startAt)\n * ```\n *\n * — and the exact answer (idle timeout, max lifetime, stop, open detached run)\n * is the reconciler's.\n */\n listLiveBetween?(window: SpendWindow): Promise<readonly SpendBoxRecord[]>\n}\n\n/**\n * Apply one fold step. Exported so a SQL implementation and an in-memory one\n * reach the same record, and so a product can unit-test its own store against\n * the canonical answer.\n *\n * The two rules worth stating out loud:\n *\n * - `observedActivityAt` only ever moves `lastActivityAt` FORWARD. A replayed\n * or out-of-order event cannot rewind the ceiling.\n * - activity later than a recorded `stoppedAt` CLEARS the stop. A box that\n * worked after the product thought it stopped is running again, and keeping\n * the stale stop would make the ceiling too tight — inventing an over-ceiling\n * finding out of the product's own bookkeeping rather than the platform's.\n */\nexport function foldSpendBoxRecord(record: SpendBoxRecord, patch: SpendBoxPatch): SpendBoxRecord {\n let lastActivityAt = record.lastActivityAt\n let stoppedAt = record.stoppedAt\n let openDetachedRunIds = record.openDetachedRunIds\n\n if (patch.observedActivityAt !== undefined) {\n lastActivityAt = Math.max(lastActivityAt, patch.observedActivityAt)\n if (stoppedAt !== null && patch.observedActivityAt > stoppedAt) stoppedAt = null\n }\n if (patch.openDetachedRunAdd !== undefined && !openDetachedRunIds.includes(patch.openDetachedRunAdd)) {\n openDetachedRunIds = [...openDetachedRunIds, patch.openDetachedRunAdd]\n }\n if (patch.openDetachedRunRemove !== undefined) {\n openDetachedRunIds = openDetachedRunIds.filter((id) => id !== patch.openDetachedRunRemove)\n }\n if (patch.stoppedAt !== undefined) {\n // Latest-wins, but never behind observed activity: a stop we are told about\n // that predates work we watched is not the stop that closed this box.\n stoppedAt = patch.stoppedAt >= lastActivityAt ? patch.stoppedAt : stoppedAt\n }\n\n return {\n ...record,\n lastActivityAt,\n stoppedAt,\n openDetachedRunIds,\n // Set-once: a deleted sandbox id never comes back, so a second observation\n // is a duplicate delivery, not a second deletion.\n deletedAt: record.deletedAt ?? patch.deletedAt ?? null,\n }\n}\n\n/** An in-memory store that also lets a test inspect and force state. */\nexport interface InMemorySpendLedgerStore extends SpendLedgerStorePort {\n /** Every record, insertion order. */\n records(): SpendBoxRecord[]\n /** Unguarded direct write — simulates a crash-shaped or platform-seeded row. */\n put(record: SpendBoxRecord): void\n}\n\n/** Create an in-memory expectation ledger. Production writers use the same port. */\nexport function createInMemorySpendLedgerStore(): InMemorySpendLedgerStore {\n const rows = new Map<string, SpendBoxRecord>()\n return {\n async load(sandboxId) {\n const row = rows.get(sandboxId)\n return row ? structuredClone(row) : null\n },\n async insert(record) {\n const stored = structuredClone(record)\n rows.set(record.sandboxId, stored)\n return structuredClone(stored)\n },\n async update(sandboxId, patch) {\n const current = rows.get(sandboxId)\n if (!current) return null\n const next = foldSpendBoxRecord(current, patch)\n rows.set(sandboxId, next)\n return structuredClone(next)\n },\n async listLiveBetween(window) {\n // Deliberately the COARSE predicate a product's SQL can express, not the\n // exact one: the idle-timeout / max-lifetime / detached-run derivation is\n // the reconciler's, and this over-returning is what proves the reconciler\n // re-filters rather than trusting whatever a store hands it.\n return [...rows.values()]\n .filter(\n (row) =>\n row.createdAt <= window.endAt && (row.deletedAt === null || row.deletedAt >= window.startAt),\n )\n .map((row) => structuredClone(row))\n },\n records() {\n return [...rows.values()].map((row) => structuredClone(row))\n },\n put(record) {\n rows.set(record.sandboxId, structuredClone(record))\n },\n }\n}\n\n/** What the product tells the ledger when it first sees a box. */\nexport interface ObserveSandboxInput {\n readonly sandboxId: string\n readonly workspaceId: string\n /** The idle timeout the product asked the platform for, seconds. */\n readonly idleTimeoutSeconds: number\n /** The max lifetime the product asked for, seconds, when it asked for one. */\n readonly maxLifetimeSeconds?: number | null\n /** Defaults to the ledger's clock. */\n readonly at?: number\n}\n\nexport interface SpendLedgerOptions {\n readonly store: SpendLedgerStorePort\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n /** Product columns written verbatim on every insert. */\n readonly extras?: Record<string, unknown>\n}\n\n/**\n * The recording half of spend verification: the product's own account of what\n * it asked the platform for.\n *\n * Every method is best-effort from the caller's point of view — a product wires\n * these into paths that must not fail because bookkeeping failed. They still\n * reject on a store error rather than swallowing it, so a caller that wants\n * fire-and-forget says so at the call site (`/sandbox`'s hook does).\n */\nexport interface SpendLedger {\n /**\n * Record that a box exists and is billable from now. Inserts on first sight,\n * and otherwise records activity — reuse and resume are both \"the platform is\n * charging for this box again\", and the record's own existence is what\n * distinguishes them, so no caller has to know which happened.\n */\n observeSandbox(input: ObserveSandboxInput): Promise<SpendBoxRecord>\n /** Record that the product saw this box do work. */\n recordActivity(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /**\n * Record that the product handed the platform work it will NOT watch finish.\n * Until the matching end is recorded, this box's ceiling cannot rest on\n * observed activity — see `computeExpectedCeiling`.\n */\n recordDetachedRunStarted(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that a detached run was confirmed finished. */\n recordDetachedRunEnded(sandboxId: string, runId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box stopped. */\n recordStopped(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n /** Record that the product knows this box was deleted. */\n recordDeleted(sandboxId: string, at?: number): Promise<SpendBoxRecord | null>\n}\n\n/** Create the recording half over a product-supplied store. */\nexport function createSpendLedger(options: SpendLedgerOptions): SpendLedger {\n const { store } = options\n const clock = options.now ?? Date.now\n\n return {\n async observeSandbox(input) {\n const at = input.at ?? clock()\n const existing = await store.load(input.sandboxId)\n if (existing) {\n const updated = await store.update(input.sandboxId, { observedActivityAt: at })\n return updated ?? existing\n }\n return await store.insert(\n {\n sandboxId: input.sandboxId,\n workspaceId: input.workspaceId,\n createdAt: at,\n idleTimeoutSeconds: input.idleTimeoutSeconds,\n maxLifetimeSeconds: input.maxLifetimeSeconds ?? null,\n lastActivityAt: at,\n openDetachedRunIds: [],\n stoppedAt: null,\n deletedAt: null,\n },\n options.extras,\n )\n },\n async recordActivity(sandboxId, at) {\n return await store.update(sandboxId, { observedActivityAt: at ?? clock() })\n },\n async recordDetachedRunStarted(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunAdd: runId,\n })\n },\n async recordDetachedRunEnded(sandboxId, runId, at) {\n return await store.update(sandboxId, {\n observedActivityAt: at ?? clock(),\n openDetachedRunRemove: runId,\n })\n },\n async recordStopped(sandboxId, at) {\n return await store.update(sandboxId, { stoppedAt: at ?? clock() })\n },\n async recordDeleted(sandboxId, at) {\n return await store.update(sandboxId, { deletedAt: at ?? clock() })\n },\n }\n}\n","import type { SpendLedger } from './store'\n\n/** Why provisioning was refused, with every number the decision used. */\nexport interface ComputeBudgetRefusal {\n readonly workspaceId: string\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for this workspace, unsigned nanodollars. */\n readonly settledNanoUsd: number\n /** How far past the cap it already is. */\n readonly overageNanoUsd: number\n readonly at: number\n}\n\n/**\n * Provisioning refused because the workspace is already past its compute cap.\n *\n * Correctable by design: every number the decision used is on the error, so a\n * product can render \"this workspace has spent $X of its $Y compute budget\" and\n * an operator can raise the cap or investigate without reading logs.\n *\n * This is the failure mode the module exists to produce. A platform billing\n * defect that used to end in a silent negative balance now ends in provisioning\n * stopping and something loud happening instead.\n */\nexport class ComputeBudgetExceededError extends Error {\n readonly workspaceId: string\n readonly limitNanoUsd: number\n readonly settledNanoUsd: number\n readonly overageNanoUsd: number\n\n constructor(refusal: ComputeBudgetRefusal) {\n super(\n `Compute budget exceeded for workspace ${refusal.workspaceId}: ` +\n `$${(refusal.settledNanoUsd / 1_000_000_000).toFixed(2)} settled against a cap of ` +\n `$${(refusal.limitNanoUsd / 1_000_000_000).toFixed(2)} ` +\n `(over by $${(refusal.overageNanoUsd / 1_000_000_000).toFixed(2)}). ` +\n 'No sandbox was provisioned. Raise the cap or reconcile the spend before retrying.',\n )\n this.name = 'ComputeBudgetExceededError'\n this.workspaceId = refusal.workspaceId\n this.limitNanoUsd = refusal.limitNanoUsd\n this.settledNanoUsd = refusal.settledNanoUsd\n this.overageNanoUsd = refusal.overageNanoUsd\n }\n}\n\n/**\n * A per-workspace cap on sandbox compute.\n *\n * `/billing`'s budget primitive caps MODEL keys, and it works because the\n * platform enforces the cap at the key it minted. Sandbox compute has no such\n * key: a box bills the shared company wallet, so nothing upstream refuses. This\n * carries the same shape to the one place a consumer can still act — the moment\n * before it asks for another box.\n *\n * `settledNanoUsd` is a callback rather than a number because the authority is\n * the platform ledger, not this package: the product reads the same rows it\n * hands the reconciler. Cache it if the read is expensive; a cap is a\n * coarse-grained control and a slightly stale total still refuses.\n */\nexport interface ComputeBudget {\n /** The cap, unsigned nanodollars. */\n readonly limitNanoUsd: number\n /** Cumulative settled compute spend for the workspace, unsigned nanodollars. */\n readonly settledNanoUsd: (workspaceId: string) => Promise<number> | number\n /**\n * Called on every refusal, before the error is thrown. This is the alert\n * seam: a refusal nobody hears is a product that silently stopped working.\n */\n readonly onRefusal?: (refusal: ComputeBudgetRefusal) => void\n /** Injectable clock (epoch ms). Default `Date.now`. */\n readonly now?: () => number\n}\n\n/**\n * Throw {@link ComputeBudgetExceededError} when the workspace is already past\n * its cap. Returns normally — and reads nothing — when no budget is configured.\n *\n * Deliberately a pre-check against spend ALREADY SETTLED, not a reservation\n * against spend about to happen: settlement lags provisioning by design (the\n * platform's durable settlement queue), so there is no instant at which a\n * consumer could hold an accurate running total. The cap therefore overshoots by\n * at most the unsettled tail, which is bounded by the box's own idle timeout.\n * A cap that refuses one box late is worth far more than one that cannot be\n * implemented honestly.\n */\nexport async function assertComputeBudget(\n budget: ComputeBudget | undefined,\n workspaceId: string,\n): Promise<void> {\n if (!budget) return\n const settledNanoUsd = await budget.settledNanoUsd(workspaceId)\n if (settledNanoUsd < budget.limitNanoUsd) return\n\n const refusal: ComputeBudgetRefusal = {\n workspaceId,\n limitNanoUsd: budget.limitNanoUsd,\n settledNanoUsd,\n overageNanoUsd: settledNanoUsd - budget.limitNanoUsd,\n at: (budget.now ?? Date.now)(),\n }\n budget.onRefusal?.(refusal)\n throw new ComputeBudgetExceededError(refusal)\n}\n\n// ── the /sandbox seam ─────────────────────────────────────────────────────────\n\n/**\n * What `/sandbox` reports once a box is provisioned, reused or resumed.\n *\n * Structurally identical to `SandboxProvisionedObservation` in `/sandbox`, and\n * deliberately re-declared rather than imported: `/spend` composes `/sandbox`,\n * so a type import in the other direction would invert the dependency. The two\n * are pinned together by a compile-time assignment in this module's tests.\n */\nexport interface SpendProvisionObservation {\n readonly workspaceId: string\n readonly userId?: string\n readonly sandboxId: string\n readonly boxKey?: string | undefined\n readonly idleTimeoutSeconds: number\n readonly maxLifetimeSeconds?: number | undefined\n readonly at: number\n}\n\n/**\n * The optional seam `EnsureWorkspaceSandboxOptions.spend` and the turn\n * primitives' `spend` option both accept. One object, wired in both places.\n */\nexport interface SandboxSpendSeam {\n beforeProvision?(input: { workspaceId: string; userId?: string }): Promise<void> | void\n onProvisioned?(observation: SpendProvisionObservation): Promise<void> | void\n /** Synchronous by contract — it sits on the turn path. See `createSandboxSpendHooks`. */\n onActivity?(input: { sandboxId: string; at: number }): void\n}\n\nexport interface SandboxSpendHooksOptions {\n /** Records box lifecycle. Omit to run the budget guard alone. */\n readonly ledger?: SpendLedger\n /** Refuses provisioning past a cap. Omit to record alone. */\n readonly budget?: ComputeBudget\n /**\n * Called when RECORDING fails. Recording is best-effort — a bookkeeping\n * failure must never take down the provisioning it is bookkeeping — so this\n * is the only place such a failure is visible. A refusal is NOT routed here;\n * refusals throw, by design.\n */\n readonly onError?: (error: unknown) => void\n}\n\n/**\n * Build the object to hand `ensureWorkspaceSandbox`'s `spend` option.\n *\n * Wiring it is the entire adoption cost: one field, and the product's boxes are\n * both budget-capped and recorded.\n */\nexport function createSandboxSpendHooks(options: SandboxSpendHooksOptions): SandboxSpendSeam {\n const { ledger, budget, onError } = options\n return {\n async beforeProvision(input) {\n await assertComputeBudget(budget, input.workspaceId)\n },\n async onProvisioned(observation) {\n if (!ledger) return\n try {\n await ledger.observeSandbox({\n sandboxId: observation.sandboxId,\n workspaceId: observation.workspaceId,\n idleTimeoutSeconds: observation.idleTimeoutSeconds,\n maxLifetimeSeconds: observation.maxLifetimeSeconds ?? null,\n at: observation.at,\n })\n } catch (err) {\n onError?.(err)\n }\n },\n onActivity(input) {\n if (!ledger) return\n // The turn path calls this synchronously and does not await it, so the\n // promise is settled here rather than escaping as an unhandled rejection.\n // Recording activity is a monotonic max, so a write that lands late — or\n // out of order against another turn's — still converges.\n void ledger.recordActivity(input.sandboxId, input.at).catch((err: unknown) => onError?.(err))\n },\n }\n}\n","/**\n * The budget-derived delegated mint — the platform's reservation contract as\n * one function.\n *\n * The platform RESERVES a delegated child key's budget against its parent's\n * REMAINING budget: an ask above remaining is a hard refusal (no amounts in\n * the response), which is how every delegated mint in a fleet product died\n * until the derivation landed (hardware-agent #165, 2026-10-09: a flat $25\n * default asked of a $5-remaining parent refused 100% of mints). Products\n * that mint delegated keys must derive the ask from what the platform\n * actually guarantees:\n *\n * - the parent's remaining budget is UNKNOWN → the configured ask stands\n * (the platform isn't reporting a ceiling; nothing better is knowable);\n * - remaining ≤ 0 → refuse with a correctable top-up error, before any\n * mint is attempted;\n * - otherwise → ask min(configured, remaining) floored to whole cents,\n * never below the platform's minimum child budget (the floor keeps the\n * mint VALID — a below-minimum ask is refused by the key API).\n *\n * Note the boundary honestly: with remaining between $0.01 and the minimum,\n * the derived ask (the floor) still exceeds remaining and the platform will\n * refuse it — that case is indistinguishable from exhausted for the caller\n * and surfaces through the same error path with the numbers attached.\n */\n\n/** The platform's minimum child-key budget. Asks below it are refused by the\n * key API, so the derivation never produces one. */\nexport const MIN_CHILD_KEY_BUDGET_USD = 0.5\n\n/** The source key is exhausted — the only refusal this contract throws.\n * Correctable by design: both numbers the decision used are on the error. */\nexport class ParentBudgetExhaustedError extends Error {\n readonly requestedUsd: number\n readonly parentRemainingUsd: number\n\n constructor(requestedUsd: number, parentRemainingUsd: number) {\n super(\n `workspace source key has no budget remaining ($${parentRemainingUsd.toFixed(2)}) — ` +\n `top up or raise its cap before sandbox turns (the mint would have asked $${requestedUsd.toFixed(2)})`,\n )\n this.name = 'ParentBudgetExhaustedError'\n this.requestedUsd = requestedUsd\n this.parentRemainingUsd = parentRemainingUsd\n }\n}\n\nexport interface DeriveChildKeyBudgetInput {\n /** The configured/default ask (the product's mint budget). Must be a\n * finite positive number. */\n readonly requestedUsd: number\n /** The parent's REMAINING budget as the platform's verify/key info\n * reports it. `null`/`undefined`/non-finite = unknown (not reported). */\n readonly parentRemainingUsd?: number | null\n /** Minimum child budget override. Default {@link MIN_CHILD_KEY_BUDGET_USD}. */\n readonly minChildUsd?: number\n}\n\nexport interface DerivedChildKeyBudget {\n /** The ask the mint should make — never above a known remaining, never\n * below the platform minimum. */\n readonly budgetUsd: number\n /** True when the ask was lowered (or floored) because the parent holds\n * less than the configured default. */\n readonly derivedFromParent: boolean\n /** True when the platform reports no ceiling and the configured ask stood. */\n readonly parentRemainingUnknown: boolean\n}\n\n/** Derive the delegated child-key budget from the parent's remaining budget.\n * Throws {@link ParentBudgetExhaustedError} when the parent is exhausted,\n * before any mint is attempted. */\nexport function deriveChildKeyBudgetUsd(input: DeriveChildKeyBudgetInput): DerivedChildKeyBudget {\n const minChildUsd = input.minChildUsd ?? MIN_CHILD_KEY_BUDGET_USD\n if (!Number.isFinite(input.requestedUsd) || input.requestedUsd <= 0) {\n throw new Error(\n `deriveChildKeyBudgetUsd: requestedUsd must be a finite positive USD amount (got ${input.requestedUsd})`,\n )\n }\n if (!Number.isFinite(minChildUsd) || minChildUsd <= 0) {\n throw new Error(\n `deriveChildKeyBudgetUsd: minChildUsd must be a finite positive USD amount (got ${minChildUsd})`,\n )\n }\n const ceiling = input.parentRemainingUsd\n const known = typeof ceiling === 'number' && Number.isFinite(ceiling)\n if (!known) {\n return { budgetUsd: input.requestedUsd, derivedFromParent: false, parentRemainingUnknown: true }\n }\n if (ceiling <= 0) throw new ParentBudgetExhaustedError(input.requestedUsd, ceiling)\n // Never above remaining (floored to whole cents), never below the platform\n // minimum: max(min, min(requested, floor(remaining·100)/100)).\n const budgetUsd = Math.max(minChildUsd, Math.min(input.requestedUsd, Math.floor(ceiling * 100) / 100))\n return {\n budgetUsd,\n derivedFromParent: budgetUsd < input.requestedUsd || ceiling < minChildUsd,\n parentRemainingUnknown: false,\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/stream/index.js
CHANGED
|
@@ -50,9 +50,9 @@ import {
|
|
|
50
50
|
terminalizeDanglingAssistantToolUpdates,
|
|
51
51
|
terminalizeDanglingToolPart,
|
|
52
52
|
terminalizeDanglingToolParts
|
|
53
|
-
} from "../chunk-
|
|
54
|
-
import "../chunk-LQYIBL3G.js";
|
|
53
|
+
} from "../chunk-ZGUXI5SI.js";
|
|
55
54
|
import "../chunk-6VQCFJWP.js";
|
|
55
|
+
import "../chunk-LQYIBL3G.js";
|
|
56
56
|
export {
|
|
57
57
|
DEFAULT_RUNNING_TURN_LEASE_MS,
|
|
58
58
|
DEFAULT_RUNNING_TURN_RENEW_INTERVAL_MS,
|