@amalgm/tools 0.1.5 → 0.1.6

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 (43) hide show
  1. package/PURPOSE.md +58 -26
  2. package/README.md +36 -10
  3. package/dist/apply-definition.d.ts +12 -0
  4. package/dist/apply-definition.js +33 -0
  5. package/dist/artifact-files.d.ts +3 -1
  6. package/dist/artifact-files.js +11 -3
  7. package/dist/artifacts.d.ts +8 -6
  8. package/dist/artifacts.js +19 -6
  9. package/dist/definition.js +10 -0
  10. package/dist/deployment-files.d.ts +10 -0
  11. package/dist/deployment-files.js +38 -0
  12. package/dist/deployment-types.d.ts +32 -0
  13. package/dist/deployment-types.js +1 -0
  14. package/dist/deployments.d.ts +19 -0
  15. package/dist/deployments.js +98 -0
  16. package/dist/host-mcp-tool.d.ts +8 -0
  17. package/dist/host-mcp-tool.js +40 -0
  18. package/dist/http.js +6 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.js +3 -1
  21. package/dist/mcp-server.d.ts +2 -1
  22. package/dist/mcp-server.js +3 -2
  23. package/dist/mcp.d.ts +2 -1
  24. package/dist/mcp.js +33 -21
  25. package/dist/notifications.js +4 -3
  26. package/dist/schema.js +15 -0
  27. package/dist/store.d.ts +17 -4
  28. package/dist/store.js +105 -41
  29. package/dist/toolbox-deployments.d.ts +30 -0
  30. package/dist/toolbox-deployments.js +125 -0
  31. package/dist/toolbox-projection.d.ts +13 -0
  32. package/dist/toolbox-projection.js +41 -0
  33. package/dist/toolbox-view.d.ts +35 -0
  34. package/dist/toolbox-view.js +75 -0
  35. package/dist/toolbox.d.ts +13 -5
  36. package/dist/toolbox.js +90 -114
  37. package/dist/types.d.ts +30 -1
  38. package/dist/updates.d.ts +2 -1
  39. package/dist/updates.js +12 -1
  40. package/docs/ENGINE_INTEGRATION.md +10 -54
  41. package/docs/SHELL_INTEGRATION.md +44 -0
  42. package/package.json +3 -2
  43. package/skills/tools/SKILL.md +185 -0
package/PURPOSE.md CHANGED
@@ -2,19 +2,20 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Amalgm Tools lets a user define, keep, select, and run agent capabilities
6
- without installing Amalgm Engine. It serves people building agents,
5
+ Amalgm Tools lets a user define, keep, select, and run agent capabilities as a
6
+ standalone product or inside Amalgm. It serves people building agents,
7
7
  automations, and applications that need one small local source of truth for
8
8
  CLI, HTTP API, and MCP tools. When the product is healthy, every callable
9
9
  action has one stable identity and one owning tool, a selected loadout exposes
10
10
  only its granted capabilities, and the SDK, CLI, and MCP surfaces all observe
11
11
  and execute the same catalog.
12
12
 
13
- Amalgm Engine is the composition root for the complete Amalgm runtime. This
14
- repository owns the Tools product and its Toolbox. Engine may provide
15
- first-party actions, credentials, agent loadouts, state events, and UI
16
- composition through public adapters, but it does not maintain a second tool
17
- registry or execution implementation.
13
+ Amalgm Shell is the active machine composition root. This repository owns the
14
+ Tools product and its Toolbox. Shell may provide first-party product
15
+ projections, credential resolution, agent loadouts, state events, and route
16
+ mounting through public adapters, but it does not maintain a second tool
17
+ registry or execution implementation. Amalgm Engine is deprecated historical
18
+ and parity evidence only.
18
19
 
19
20
  ## Primitives
20
21
 
@@ -23,13 +24,18 @@ registry or execution implementation.
23
24
  - An **action** is one model-callable capability owned by exactly one tool.
24
25
  - A **Toolbox** is the sole catalog and mutation authority for one local state
25
26
  directory.
27
+ - A **tool deployment** is one immutable definition of a stable tool identity,
28
+ including its complete authoritative action set; activation appends the
29
+ deployment and moves that tool's current pointer atomically.
26
30
  - A **loadout** is a set of tool or action ids used to derive a read-only view
27
31
  of the Toolbox.
28
32
  - A **driver** executes actions for one tool type.
33
+ - An **MCP server** is a separately identified runtime endpoint that may expose
34
+ one or more tools; selecting a tool never changes that server's identity.
29
35
  - A **system tool** is an embedder-owned definition projected into the catalog;
30
36
  it is not user state.
31
- - A **notification** is one request to inform the current user; a channel
32
- adapter turns that intent into delivery.
37
+ - A **notification compatibility adapter** is the retained legacy projection
38
+ of `notifications.notify_user`; Channels owns current durable delivery.
33
39
 
34
40
  ## Axioms
35
41
 
@@ -40,13 +46,14 @@ locked dependency graph on the supported Node toolchain.
40
46
  `<tool-id>.<action-name>`.
41
47
  2. Tool ids are stable, globally unique within one Toolbox, and never change
42
48
  as a side effect of renaming display text.
43
- 3. A tool's portable definition is its artifact file: one file per tool,
44
- the tool and all of its actions together, never a second tool in the
45
- same file. The Toolbox database is a derived index maintained for
46
- runtime and realtime sync; SDK, CLI, and MCP adapters never keep
47
- parallel registries.
48
- 4. Applying a tool definition is atomic and authoritative for that tool: its
49
- supplied action set replaces the previous set completely.
49
+ 3. A tool's portable source of truth is its immutable deployment history and
50
+ current deployment pointer. SQLite tables, WAL files, catalog indexes, and
51
+ compatibility artifacts are local projections; SDK, CLI, MCP, HTTP, and UI
52
+ adapters never keep parallel registries.
53
+ 4. Applying a semantically changed tool definition mints one deployment and
54
+ atomically activates its complete action set. Applying equal semantics is a
55
+ no-op; rollback and deletion mint new deployments rather than rewriting or
56
+ removing history.
50
57
  5. Deleting a tool deletes all of its actions in the same transaction; an
51
58
  orphan action can never be observed.
52
59
  6. Disabled tools expose and execute no actions. Disabled actions are likewise
@@ -71,27 +78,52 @@ locked dependency graph on the supported Node toolchain.
71
78
  15. System tools are immutable projections supplied by the embedder. Removing
72
79
  an embedder projection removes it from the catalog without mutating user
73
80
  state.
74
- 16. The SDK owns behavior. CLI, MCP, HTTP, UI, and Engine integrations only
75
- adapt the public Toolbox service.
81
+ 16. The SDK owns behavior. CLI, MCP, HTTP, UI, Shell, and other host
82
+ integrations only adapt the public Toolbox service.
76
83
  17. TypeScript under `src/` is the product source of truth. JavaScript and
77
84
  declarations under `dist/` are generated together and are never edited by
78
85
  hand.
79
86
  18. Tools may store opaque references owned by another product, but neither
80
87
  Tools nor a Core adapter silently rewrites that product. A cross-product
81
88
  workflow has one explicit product owner.
82
- 19. `notifications.notify_user` addresses only the current user. Callers may
83
- supply content and severity, never a recipient or delivery credential.
84
- 20. Notification channels are host capabilities. Tools owns validation and
85
- result semantics; an injected channel adapter owns formatting, recipient
86
- lookup, credentials, and delivery.
89
+ 19. The retained `notifications.notify_user` compatibility adapter addresses
90
+ only the current user. Callers may supply content and severity, never a
91
+ recipient or delivery credential.
92
+ 20. Channels owns current notification validation, persistence, formatting,
93
+ recipient lookup, credentials, and delivery. Tools may project Channels'
94
+ official MCP descriptors or expose its explicitly legacy email adapter; it
95
+ never becomes a second durable delivery authority.
96
+ 21. Reopening a Toolbox preserves the temporal fields of every semantically
97
+ unchanged system projection already present in its portable index; boot
98
+ never rewrites tracked bytes merely to stamp the current machine's time.
99
+ 22. The Toolbox exposes the versioned `deployment.activate` operation and its
100
+ exact definition through one Live surface; SQLite pages are never part of
101
+ that contract. A received activation materializes locally without echo,
102
+ retries are idempotent, and an existing deployment id can never acquire
103
+ different bytes.
104
+ 23. A host may separate the synced deployment directory from its machine-local
105
+ database. Amalgm Shell stores immutable deployment documents beneath
106
+ `users/<email>/toolbox/<tool-id>/deployments/` while keeping SQLite
107
+ coordination state below the machine boundary.
108
+ 24. A Chat preparation selects one catalog `revisionId` derived from exact
109
+ current deployment ids and system projections. Preparation either resolves
110
+ that exact revision or fails; it never silently substitutes newer tools.
111
+ 25. A host MCP source names both its server and its route. Tool identity,
112
+ action identity, server identity, and transport location are distinct even
113
+ when a first-party release currently maps them one-to-one.
114
+ 26. A scoped Toolbox MCP server advertises and calls only the selected loadout;
115
+ management actions are included only when the composing host asks for them.
87
116
 
88
117
  ## Predictable behavior
89
118
 
90
- Applying the same definition twice produces the same catalog. Applying a
91
- changed action set replaces the old set atomically. Selecting a whole tool
119
+ Applying the same definition twice produces the same catalog and no second
120
+ deployment. Reopening the
121
+ same system projection produces the same portable index bytes on every
122
+ machine. Applying a changed action set replaces the old set atomically.
123
+ Selecting a whole tool
92
124
  grants all of its enabled actions; selecting one action grants only that
93
125
  action. Disabling or deleting a tool immediately removes all of its actions
94
126
  from list and call surfaces. CLI arguments are passed directly to a process,
95
127
  API paths stay under their registered origin, and MCP calls cross only the
96
- driver boundary supplied by the composing host. Engine and standalone users
128
+ driver boundary supplied by the composing host. Shell and standalone users
97
129
  therefore get the same identities, validation, selection, and execution rules.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Amalgm Tools
2
2
 
3
- `@amalgm/tools` is the standalone Tools product from Amalgm Engine. It owns
4
- tool definitions, the local Toolbox catalog, loadout selection, execution,
5
- and MCP projection. It can be used without the rest of Amalgm.
3
+ `@amalgm/tools` is the standalone Tools product. It owns tool definitions, the
4
+ local Toolbox catalog, immutable deployments, loadout selection, execution,
5
+ and MCP projection. It can be used without the rest of Amalgm and is composed
6
+ into the machine product by `@amalgm/shell`.
6
7
 
7
8
  ```js
8
9
  import { defineTool, Toolbox } from '@amalgm/tools';
@@ -36,6 +37,28 @@ verbatim when set, then `AMALGM_DIR/toolbox`, then the scoped layout
36
37
  `~/.amalgm/users/<scope>/toolbox`. Passing `stateDir` or `databaseFile`
37
38
  bypasses resolution entirely.
38
39
 
40
+ Platform hosts should separate portable deployment documents from local
41
+ coordination state:
42
+
43
+ ```js
44
+ const toolbox = new Toolbox({
45
+ deploymentDir: '/amalgm/users/person@example.com/toolbox',
46
+ databaseFile: '/machine-state/services/tools/tools.db',
47
+ });
48
+ ```
49
+
50
+ An intentional semantic change appends
51
+ `<deploymentDir>/<tool-id>/deployments/<deployment-id>.json` and atomically
52
+ advances the local head. The database, WAL, `toolbox.index.json`, and legacy
53
+ flat artifacts are projections and must not be synchronized. `catalog()`
54
+ exposes both a local numeric `revision` for presentation freshness and the
55
+ immutable `revisionId` that Chat must use for preparation.
56
+
57
+ `deploymentSurface()` implements the `amalgm.tools.deployments@1` Live
58
+ surface. Its operation is the shared `deployment.activate` envelope. Remote
59
+ application is idempotent and does not echo; `deploymentSnapshot()` and
60
+ `applyDeploymentSnapshot()` are the hydration/recovery boundary.
61
+
39
62
  ## CLI
40
63
 
41
64
  ```sh
@@ -60,18 +83,20 @@ const server = createToolboxHttpServer({ toolbox });
60
83
  await server.listen(8083);
61
84
  ```
62
85
 
63
- The read adapter serves the Engine-compatible `GET /toolbox`,
86
+ The read adapter serves the legacy-compatible `GET /toolbox`,
64
87
  `GET /toolbox/tools?id=...`, and `GET /toolbox/actions?id=...` routes over
65
88
  the same live Toolbox used by CLI and MCP consumers. Mutation routes return
66
89
  405 until their legacy record fields can be represented without data loss.
67
90
 
68
91
  ## Notifications
69
92
 
70
- `Notifications` projects the first-party `notifications.notify_user` action
71
- into the Toolbox. The action validates one channel-neutral request and hands
72
- it to an injected email delivery adapter; recipient lookup and credentials
73
- never enter the tool record. `createNotificationsHttpServer` exposes
74
- `GET /email` as the channel capability document used by the shell.
93
+ Channels is the current owner of durable notification delivery. Shell projects
94
+ the official `@amalgm/channels` MCP descriptors into Toolbox as the
95
+ `channels` first-party tool. Tools still exports `Notifications`,
96
+ `notificationToolDefinition`, and `createNotificationsHttpServer` as a legacy
97
+ compatibility adapter for the retired `notifications.notify_user` and
98
+ `GET /email` shapes; that adapter is not the current platform delivery store
99
+ and must not be presented as a second Channels implementation.
75
100
 
76
101
  ## Tool types
77
102
 
@@ -83,4 +108,5 @@ never enter the tool record. `createNotificationsHttpServer` exposes
83
108
  Secret values are never part of definitions. `secretEnv` and `secretHeaders`
84
109
  contain opaque references resolved through `CallOptions.resolveSecret`.
85
110
  See [PURPOSE.md](./PURPOSE.md) for the complete contract and
86
- [ENGINE_INTEGRATION.md](./docs/ENGINE_INTEGRATION.md) for composition.
111
+ [SHELL_INTEGRATION.md](./docs/SHELL_INTEGRATION.md) for active composition.
112
+ The [Engine integration](./docs/ENGINE_INTEGRATION.md) page is historical only.
@@ -0,0 +1,12 @@
1
+ import { Store } from './store.js';
2
+ import { ToolboxDeployments } from './toolbox-deployments.js';
3
+ import type { ActionRecord, ApplyResult, ToolDefinition, ToolRecord } from './types.js';
4
+ /** Normalize and deploy one authoritative user definition. */
5
+ declare function applyDefinition(options: {
6
+ definition: ToolDefinition;
7
+ store: Store;
8
+ deployments: ToolboxDeployments;
9
+ systemTools: readonly ToolRecord[];
10
+ systemActions: readonly ActionRecord[];
11
+ }): Promise<ApplyResult>;
12
+ export { applyDefinition };
@@ -0,0 +1,33 @@
1
+ import { portableRecordSemantic } from './artifacts.js';
2
+ import { normalizeDefinition } from './definition.js';
3
+ import { assertMcpNamesUnique, id } from './ids.js';
4
+ /** Normalize and deploy one authoritative user definition. */
5
+ async function applyDefinition(options) {
6
+ const { definition, store, deployments, systemTools, systemActions } = options;
7
+ const normalizedId = id(definition.id, 'tool.id', 64);
8
+ if (definition.origin === 'system')
9
+ throw new Error('System tools can only be supplied by the embedder');
10
+ if (systemTools.some((tool) => tool.id === normalizedId))
11
+ throw new Error(`System tool is immutable: ${normalizedId}`);
12
+ const existing = store.getTool(normalizedId);
13
+ const oldActions = new Map(store.actionsFor(normalizedId).map((action) => [action.id, action]));
14
+ const normalized = normalizeDefinition(definition, existing || undefined);
15
+ normalized.actions = normalized.actions.map((action) => ({
16
+ ...action, createdAt: oldActions.get(action.id)?.createdAt || action.createdAt,
17
+ }));
18
+ assertMcpNamesUnique([
19
+ ...systemActions,
20
+ ...store.catalog().actions.filter((action) => action.toolId !== normalized.tool.id),
21
+ ...normalized.actions,
22
+ ]);
23
+ const unchanged = existing && portableRecordSemantic(existing) === portableRecordSemantic(normalized.tool)
24
+ && normalized.actions.length === oldActions.size
25
+ && normalized.actions.every((action) => portableRecordSemantic(action)
26
+ === portableRecordSemantic(oldActions.get(action.id)));
27
+ if (unchanged) {
28
+ return { tool: existing, actions: [...oldActions.values()].sort((a, b) => a.id.localeCompare(b.id)) };
29
+ }
30
+ await deployments.activateLocal(normalized);
31
+ return normalized;
32
+ }
33
+ export { applyDefinition };
@@ -1,3 +1,4 @@
1
+ import type { ToolboxIndexDocument } from './artifacts.js';
1
2
  import type { ActionRecord, Catalog, ToolRecord } from './types.js';
2
3
  declare class ArtifactFiles {
3
4
  private readonly directory;
@@ -9,9 +10,10 @@ declare class ArtifactFiles {
9
10
  }): string;
10
11
  removeTool(toolId: string): void;
11
12
  writeIndex(catalog: Catalog): void;
13
+ readIndex(): ToolboxIndexDocument | null;
12
14
  /** Give every persisted user tool its portable artifact and refresh the
13
15
  * index. Additive only: a file for a tool this catalog does not know is
14
- * never deleted here — removal is an explicit mutation. */
16
+ * never deleted here — removal or snapshot hydration is explicit. */
15
17
  materialize(catalog: Catalog): void;
16
18
  /** Execute the legacy `toolbox.json` migration plan: per-tool artifacts
17
19
  * first, then the index, and only then the rename that retires the
@@ -2,8 +2,9 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { LEGACY_TOOLBOX_FILE_NAME, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, userArtifactFileName, } from './artifacts.js';
4
4
  /**
5
- * The host adapter for tool artifacts: it executes the pure documents and
6
- * plans from `artifacts.ts` against one state directory. Writes are atomic
5
+ * The host adapter for current compatibility projections: it executes the
6
+ * pure documents and plans from `artifacts.ts` against one local state
7
+ * directory. Writes are atomic
7
8
  * (tmp file + rename) and skipped when the bytes are unchanged, matching
8
9
  * Engine `lib/storage.js`.
9
10
  *
@@ -51,9 +52,16 @@ class ArtifactFiles {
51
52
  writeIndex(catalog) {
52
53
  writeJsonIfChanged(this.file(TOOLBOX_INDEX_FILE_NAME), catalogIndexDocument(catalog));
53
54
  }
55
+ readIndex() {
56
+ const value = readJson(this.file(TOOLBOX_INDEX_FILE_NAME));
57
+ if (!value || value.version !== 1 || !value.tools || typeof value.tools !== 'object'
58
+ || !value.toolActions || typeof value.toolActions !== 'object')
59
+ return null;
60
+ return value;
61
+ }
54
62
  /** Give every persisted user tool its portable artifact and refresh the
55
63
  * index. Additive only: a file for a tool this catalog does not know is
56
- * never deleted here — removal is an explicit mutation. */
64
+ * never deleted here — removal or snapshot hydration is explicit. */
57
65
  materialize(catalog) {
58
66
  for (const tool of catalog.tools) {
59
67
  if (tool.origin === 'system')
@@ -2,11 +2,11 @@ import type { ActionRecord, Catalog, ToolRecord } from './types.js';
2
2
  /**
3
3
  * Tool artifact format laws — the portable representation of a tool.
4
4
  *
5
- * One tool is one artifact file: the tool definition and every action it
6
- * owns travel together, and a file never carries a second tool. Artifact
7
- * files are the portable definition; the Toolbox database and
8
- * `toolbox.index.json` are derived caches. The format is ported byte-exact
9
- * from Engine `runtime/scripts/amalgm-mcp/toolbox/artifacts.js`.
5
+ * One compatibility artifact projects one current tool and every action it
6
+ * owns together; a file never carries a second tool. Immutable deployment
7
+ * documents are the portable source of truth. These artifacts, the Toolbox
8
+ * database, and `toolbox.index.json` are derived views retained for current
9
+ * Engine/UI readers.
10
10
  *
11
11
  * Pure laws only: nothing here touches the filesystem. `artifact-files.ts`
12
12
  * is the host adapter that executes these documents and plans.
@@ -57,10 +57,12 @@ declare function artifactDocument(input: {
57
57
  declare function isToolArtifactDocument(value: unknown): value is ToolArtifactDocument;
58
58
  declare function toolboxIndexDocument(tools: Record<string, unknown>, toolActions: Record<string, unknown>): ToolboxIndexDocument;
59
59
  declare function catalogIndexDocument(catalog: Pick<Catalog, 'tools' | 'actions'>): ToolboxIndexDocument;
60
+ declare function portableRecordSemantic(value: ToolRecord | ActionRecord): string;
61
+ declare function preserveProjectionTime<T extends ToolRecord | ActionRecord>(candidate: T, previous: unknown): T;
60
62
  /** The migration of one legacy aggregate `toolbox.json` catalog, as a pure
61
63
  * plan: every user tool becomes a per-tool artifact write, the whole legacy
62
64
  * catalog becomes the index, and the aggregate file is renamed out of the
63
65
  * way only after each record has a per-tool replacement. */
64
66
  declare function legacyMigrationPlan(legacy: unknown): LegacyMigrationPlan;
65
- export { LEGACY_TOOLBOX_BACKUP_FILE_NAME, LEGACY_TOOLBOX_FILE_NAME, TOOL_ARTIFACT_KIND, TOOL_ARTIFACT_SCHEMA_VERSION, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, toolboxIndexDocument, userArtifactFileName, };
67
+ export { LEGACY_TOOLBOX_BACKUP_FILE_NAME, LEGACY_TOOLBOX_FILE_NAME, TOOL_ARTIFACT_KIND, TOOL_ARTIFACT_SCHEMA_VERSION, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, portableRecordSemantic, preserveProjectionTime, toolboxIndexDocument, userArtifactFileName, };
66
68
  export type { ArtifactFileWrite, LegacyMigrationPlan, ToolArtifactDocument, ToolboxIndexDocument };
package/dist/artifacts.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Tool artifact format laws — the portable representation of a tool.
3
3
  *
4
- * One tool is one artifact file: the tool definition and every action it
5
- * owns travel together, and a file never carries a second tool. Artifact
6
- * files are the portable definition; the Toolbox database and
7
- * `toolbox.index.json` are derived caches. The format is ported byte-exact
8
- * from Engine `runtime/scripts/amalgm-mcp/toolbox/artifacts.js`.
4
+ * One compatibility artifact projects one current tool and every action it
5
+ * owns together; a file never carries a second tool. Immutable deployment
6
+ * documents are the portable source of truth. These artifacts, the Toolbox
7
+ * database, and `toolbox.index.json` are derived views retained for current
8
+ * Engine/UI readers.
9
9
  *
10
10
  * Pure laws only: nothing here touches the filesystem. `artifact-files.ts`
11
11
  * is the host adapter that executes these documents and plans.
@@ -81,6 +81,19 @@ function toolboxIndexDocument(tools, toolActions) {
81
81
  function catalogIndexDocument(catalog) {
82
82
  return toolboxIndexDocument(Object.fromEntries(catalog.tools.map((tool) => [tool.id, tool])), Object.fromEntries(catalog.actions.map((action) => [action.id, action])));
83
83
  }
84
+ function portableRecordSemantic(value) {
85
+ const { createdAt, updatedAt, ...portable } = value;
86
+ return JSON.stringify(portable);
87
+ }
88
+ function preserveProjectionTime(candidate, previous) {
89
+ if (!previous || typeof previous !== 'object')
90
+ return candidate;
91
+ const record = previous;
92
+ if (typeof record.createdAt !== 'string' || typeof record.updatedAt !== 'string'
93
+ || portableRecordSemantic(record) !== portableRecordSemantic(candidate))
94
+ return candidate;
95
+ return { ...candidate, createdAt: record.createdAt, updatedAt: record.updatedAt };
96
+ }
84
97
  /** The migration of one legacy aggregate `toolbox.json` catalog, as a pure
85
98
  * plan: every user tool becomes a per-tool artifact write, the whole legacy
86
99
  * catalog becomes the index, and the aggregate file is renamed out of the
@@ -109,4 +122,4 @@ function legacyMigrationPlan(legacy) {
109
122
  rename: { from: LEGACY_TOOLBOX_FILE_NAME, to: LEGACY_TOOLBOX_BACKUP_FILE_NAME },
110
123
  };
111
124
  }
112
- export { LEGACY_TOOLBOX_BACKUP_FILE_NAME, LEGACY_TOOLBOX_FILE_NAME, TOOL_ARTIFACT_KIND, TOOL_ARTIFACT_SCHEMA_VERSION, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, toolboxIndexDocument, userArtifactFileName, };
125
+ export { LEGACY_TOOLBOX_BACKUP_FILE_NAME, LEGACY_TOOLBOX_FILE_NAME, TOOL_ARTIFACT_KIND, TOOL_ARTIFACT_SCHEMA_VERSION, TOOLBOX_INDEX_FILE_NAME, artifactDocument, catalogIndexDocument, isToolArtifactDocument, legacyMigrationPlan, safeToolId, portableRecordSemantic, preserveProjectionTime, toolboxIndexDocument, userArtifactFileName, };
@@ -32,6 +32,15 @@ function refs(value, label) {
32
32
  result[string(key, label)] = string(reference, `${label}.${key}`);
33
33
  return result;
34
34
  }
35
+ function hostRoute(value) {
36
+ if (value === undefined)
37
+ return undefined;
38
+ const route = string(value, 'source.route');
39
+ if (!route.startsWith('/') || route.startsWith('//') || route.includes('?') || route.includes('#')) {
40
+ throw new Error('source.route must be an absolute path without query or fragment');
41
+ }
42
+ return route.replace(/\/+$/, '') || '/';
43
+ }
35
44
  function base(source) {
36
45
  return {
37
46
  timeoutMs: positive(source.timeoutMs, DEFAULT_TIMEOUT, 'source.timeoutMs'),
@@ -91,6 +100,7 @@ function normalizeSource(source) {
91
100
  ...(strings(source.args, 'source.args') ? { args: strings(source.args, 'source.args') } : {}),
92
101
  ...(source.cwd ? { cwd: string(source.cwd, 'source.cwd') } : {}),
93
102
  ...(source.serverName ? { serverName: string(source.serverName, 'source.serverName') } : {}),
103
+ ...(hostRoute(source.route) ? { route: hostRoute(source.route) } : {}),
94
104
  ...(refs(source.secretHeaders, 'source.secretHeaders') ? { secretHeaders: refs(source.secretHeaders, 'source.secretHeaders') } : {}),
95
105
  ...(refs(source.secretEnv, 'source.secretEnv') ? { secretEnv: refs(source.secretEnv, 'source.secretEnv') } : {}),
96
106
  };
@@ -0,0 +1,10 @@
1
+ import type { ToolDeployment } from './types.js';
2
+ /** Host effect for the portable, immutable deployment document tree. */
3
+ declare class DeploymentFiles {
4
+ private readonly root;
5
+ constructor(root: string);
6
+ file(deployment: ToolDeployment): string;
7
+ write(deployment: ToolDeployment): string;
8
+ materialize(deployments: readonly ToolDeployment[]): void;
9
+ }
10
+ export { DeploymentFiles };
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { canonicalJson } from './deployments.js';
4
+ /** Host effect for the portable, immutable deployment document tree. */
5
+ class DeploymentFiles {
6
+ root;
7
+ constructor(root) {
8
+ this.root = root;
9
+ }
10
+ file(deployment) {
11
+ return path.join(this.root, deployment.subjectId, 'deployments', `${deployment.id}.json`);
12
+ }
13
+ write(deployment) {
14
+ const target = this.file(deployment);
15
+ const bytes = `${JSON.stringify(deployment, null, 2)}\n`;
16
+ try {
17
+ const existing = fs.readFileSync(target, 'utf8');
18
+ if (canonicalJson(JSON.parse(existing)) !== canonicalJson(deployment)) {
19
+ throw new Error(`Deployment file is immutable: ${deployment.id}`);
20
+ }
21
+ return target;
22
+ }
23
+ catch (error) {
24
+ if (error.code !== 'ENOENT')
25
+ throw error;
26
+ }
27
+ fs.mkdirSync(path.dirname(target), { recursive: true });
28
+ const temporary = `${target}.tmp`;
29
+ fs.writeFileSync(temporary, bytes, { flag: 'wx' });
30
+ fs.renameSync(temporary, target);
31
+ return target;
32
+ }
33
+ materialize(deployments) {
34
+ for (const deployment of deployments)
35
+ this.write(deployment);
36
+ }
37
+ }
38
+ export { DeploymentFiles };
@@ -0,0 +1,32 @@
1
+ import type { ApplyResult } from './types.js';
2
+ interface ToolDeployment {
3
+ schemaVersion: 1;
4
+ kind: 'amalgm-tool-deployment';
5
+ id: string;
6
+ /** The stable tool id, named generically to match @amalgm/live. */
7
+ subjectId: string;
8
+ previousDeploymentId: string | null;
9
+ definition: ApplyResult | null;
10
+ contentHash: string;
11
+ createdAt: string;
12
+ }
13
+ interface ToolDeploymentActivation {
14
+ type: 'deployment.activate';
15
+ deployment: ToolDeployment;
16
+ }
17
+ interface ToolDeploymentSnapshot {
18
+ version: 1;
19
+ deployments: Record<string, ToolDeployment>;
20
+ heads: Record<string, string>;
21
+ }
22
+ interface ToolDeploymentSurface {
23
+ readonly contract: 'amalgm.tools.deployments@1';
24
+ observeEdits(emit: (operation: ToolDeploymentActivation) => void): () => void;
25
+ applyRemote(operation: ToolDeploymentActivation, context: {
26
+ readonly hydrating: boolean;
27
+ }): void | Promise<void>;
28
+ applySnapshot(snapshot: ToolDeploymentSnapshot, context: {
29
+ readonly reason: 'hydration' | 'recovery';
30
+ }): void | Promise<void>;
31
+ }
32
+ export type { ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolDeploymentSurface, };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { ActionRecord, ApplyResult, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolRecord } from './types.js';
2
+ declare const TOOL_DEPLOYMENT_CONTRACT: "amalgm.tools.deployments@1";
3
+ declare const TOOL_DEPLOYMENT_KIND: "amalgm-tool-deployment";
4
+ declare const TOOL_DEPLOYMENT_SCHEMA_VERSION: 1;
5
+ declare function canonicalJson(value: unknown): string;
6
+ declare function createToolDeployment(input: {
7
+ toolId: string;
8
+ previousDeploymentId: string | null;
9
+ definition: ApplyResult | null;
10
+ createdAt: string;
11
+ }): ToolDeploymentActivation;
12
+ declare function assertToolDeploymentActivation(value: unknown): asserts value is ToolDeploymentActivation;
13
+ declare function deploymentRevisionId(input: {
14
+ heads: Readonly<Record<string, string>>;
15
+ systemTools?: readonly ToolRecord[];
16
+ systemActions?: readonly ActionRecord[];
17
+ }): string;
18
+ declare function emptyToolDeploymentSnapshot(): ToolDeploymentSnapshot;
19
+ export { TOOL_DEPLOYMENT_CONTRACT, TOOL_DEPLOYMENT_KIND, TOOL_DEPLOYMENT_SCHEMA_VERSION, assertToolDeploymentActivation, canonicalJson, createToolDeployment, deploymentRevisionId, emptyToolDeploymentSnapshot, };
@@ -0,0 +1,98 @@
1
+ import { createHash } from 'node:crypto';
2
+ const TOOL_DEPLOYMENT_CONTRACT = 'amalgm.tools.deployments@1';
3
+ const TOOL_DEPLOYMENT_KIND = 'amalgm-tool-deployment';
4
+ const TOOL_DEPLOYMENT_SCHEMA_VERSION = 1;
5
+ function canonical(value) {
6
+ if (Array.isArray(value))
7
+ return value.map(canonical);
8
+ if (!value || typeof value !== 'object')
9
+ return value;
10
+ return Object.fromEntries(Object.entries(value)
11
+ .filter(([, item]) => item !== undefined)
12
+ .sort(([left], [right]) => left.localeCompare(right))
13
+ .map(([key, item]) => [key, canonical(item)]));
14
+ }
15
+ function canonicalJson(value) {
16
+ return JSON.stringify(canonical(value));
17
+ }
18
+ function sha256(value) {
19
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
20
+ }
21
+ function createToolDeployment(input) {
22
+ const contentHash = sha256(input.definition);
23
+ const identity = sha256({
24
+ subjectId: input.toolId,
25
+ previousDeploymentId: input.previousDeploymentId,
26
+ contentHash,
27
+ createdAt: input.createdAt,
28
+ });
29
+ return Object.freeze({
30
+ type: 'deployment.activate',
31
+ deployment: Object.freeze({
32
+ schemaVersion: TOOL_DEPLOYMENT_SCHEMA_VERSION,
33
+ kind: TOOL_DEPLOYMENT_KIND,
34
+ id: `tooldep_${identity}`,
35
+ subjectId: input.toolId,
36
+ previousDeploymentId: input.previousDeploymentId,
37
+ definition: input.definition,
38
+ contentHash,
39
+ createdAt: input.createdAt,
40
+ }),
41
+ });
42
+ }
43
+ function assertRecord(value, label) {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value))
45
+ throw new Error(`${label} must be an object`);
46
+ }
47
+ function assertToolDeploymentActivation(value) {
48
+ assertRecord(value, 'Tool deployment activation');
49
+ if (value.type !== 'deployment.activate')
50
+ throw new Error('Tool deployment operation must be deployment.activate');
51
+ assertRecord(value.deployment, 'Tool deployment');
52
+ const deployment = value.deployment;
53
+ if (deployment.schemaVersion !== TOOL_DEPLOYMENT_SCHEMA_VERSION || deployment.kind !== TOOL_DEPLOYMENT_KIND) {
54
+ throw new Error('Unsupported tool deployment document');
55
+ }
56
+ if (typeof deployment.id !== 'string' || !deployment.id
57
+ || typeof deployment.subjectId !== 'string' || !deployment.subjectId
58
+ || typeof deployment.createdAt !== 'string' || !deployment.createdAt
59
+ || (deployment.previousDeploymentId !== null && typeof deployment.previousDeploymentId !== 'string')) {
60
+ throw new Error('Tool deployment identity is incomplete');
61
+ }
62
+ if (deployment.definition !== null) {
63
+ assertRecord(deployment.definition, 'Tool deployment definition');
64
+ assertRecord(deployment.definition.tool, 'Tool deployment tool');
65
+ if (deployment.definition.tool.id !== deployment.subjectId) {
66
+ throw new Error('Tool deployment subject does not match its tool');
67
+ }
68
+ if (deployment.definition.tool.origin === 'system') {
69
+ throw new Error('System tools cannot arrive as user deployments');
70
+ }
71
+ if (!Array.isArray(deployment.definition.actions)
72
+ || deployment.definition.actions.some((action) => action.toolId !== deployment.subjectId)) {
73
+ throw new Error('Tool deployment actions must all belong to its subject');
74
+ }
75
+ }
76
+ if (deployment.contentHash !== sha256(deployment.definition)) {
77
+ throw new Error(`Tool deployment content hash is invalid: ${deployment.id}`);
78
+ }
79
+ const expected = createToolDeployment({
80
+ toolId: deployment.subjectId,
81
+ previousDeploymentId: deployment.previousDeploymentId,
82
+ definition: deployment.definition,
83
+ createdAt: deployment.createdAt,
84
+ }).deployment.id;
85
+ if (deployment.id !== expected)
86
+ throw new Error(`Tool deployment id is invalid: ${deployment.id}`);
87
+ }
88
+ function deploymentRevisionId(input) {
89
+ return `toolbox_${sha256({
90
+ heads: input.heads,
91
+ systemTools: input.systemTools || [],
92
+ systemActions: input.systemActions || [],
93
+ })}`;
94
+ }
95
+ function emptyToolDeploymentSnapshot() {
96
+ return { version: 1, deployments: {}, heads: {} };
97
+ }
98
+ export { TOOL_DEPLOYMENT_CONTRACT, TOOL_DEPLOYMENT_KIND, TOOL_DEPLOYMENT_SCHEMA_VERSION, assertToolDeploymentActivation, canonicalJson, createToolDeployment, deploymentRevisionId, emptyToolDeploymentSnapshot, };
@@ -0,0 +1,8 @@
1
+ import type { HostMcpToolDefinitionInput, ToolDefinition } from './types.js';
2
+ /**
3
+ * Project an owning SDK's MCP descriptors into one immutable first-party
4
+ * Toolbox tool. The descriptors remain authoritative; this adapter does not
5
+ * copy or reinterpret action schemas.
6
+ */
7
+ declare function defineHostMcpTool(input: HostMcpToolDefinitionInput): ToolDefinition;
8
+ export { defineHostMcpTool };