@try-works/dsh-model-as-tool 0.1.2 → 0.3.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.
- package/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode.mjs +202 -0
- package/catalog/desert-ant-models.json +693 -693
- package/client/client.model-tools.js +592 -0
- package/client/index.js +1679 -0
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +4 -4
- package/lib/bindings.d.ts.map +1 -1
- package/lib/bindings.js +26 -25
- package/lib/capability.d.ts.map +1 -1
- package/lib/capability.js +17 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/diagnostics.d.ts +166 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +135 -0
- package/lib/extension-dependencies.d.ts +197 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +369 -0
- package/lib/extension-dispatch.d.ts +117 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +124 -0
- package/lib/extension-registrar.d.ts +100 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +134 -0
- package/lib/extensions-api.d.ts +217 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +54 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +66 -0
- package/lib/extensions-service.d.ts +334 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +1351 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +95 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +157 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +349 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +136 -0
- package/lib/index.d.ts +250 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2625 -70
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/managed-workers.d.ts +151 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +249 -0
- package/lib/mcp/client-grant.d.ts +74 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +60 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +454 -0
- package/lib/mcp/host-binding.d.ts +25 -0
- package/lib/mcp/host-binding.d.ts.map +1 -1
- package/lib/mcp/host-binding.js +107 -14
- package/lib/mcp/http.d.ts +68 -18
- package/lib/mcp/http.d.ts.map +1 -1
- package/lib/mcp/http.js +390 -62
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +46 -7
- package/lib/mcp/protocol.d.ts.map +1 -1
- package/lib/mcp/protocol.js +63 -16
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +181 -0
- package/lib/mcp/server.d.ts +11 -1
- package/lib/mcp/server.d.ts.map +1 -1
- package/lib/mcp/server.js +80 -13
- package/lib/mcp/stdio.d.ts.map +1 -1
- package/lib/mcp/stdio.js +9 -1
- package/lib/mcp/subscriptions.d.ts +43 -1
- package/lib/mcp/subscriptions.d.ts.map +1 -1
- package/lib/mcp/subscriptions.js +90 -9
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/planning.d.ts +246 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +457 -0
- package/lib/prepared-assets.d.ts +178 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +286 -0
- package/lib/provider-bridge.d.ts +37 -0
- package/lib/provider-bridge.d.ts.map +1 -1
- package/lib/provider-bridge.js +189 -12
- package/lib/ptc-authority.d.ts +140 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +216 -0
- package/lib/ptc-runtime.d.ts +156 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +191 -0
- package/lib/publication.d.ts +2 -1
- package/lib/publication.d.ts.map +1 -1
- package/lib/publication.js +13 -5
- package/lib/receipt.d.ts.map +1 -1
- package/lib/receipt.js +8 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-mode-run.d.ts +235 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +283 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +48 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +107 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +34 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +50 -17
- package/remote/spec.mjs +439 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
- package/lib/adapters/tongue.d.ts +0 -31
- package/lib/adapters/tongue.d.ts.map +0 -1
- package/lib/adapters/tongue.js +0 -93
- package/lib/adapters/voz.d.ts +0 -28
- package/lib/adapters/voz.d.ts.map +0 -1
- package/lib/adapters/voz.js +0 -102
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation and deactivation (EXT-04, EXT-26, EXT-27, EXT-28).
|
|
3
|
+
*
|
|
4
|
+
* The two orderings this module exists to enforce:
|
|
5
|
+
*
|
|
6
|
+
* - **Activate commits last.** Contributions are published to a candidate set,
|
|
7
|
+
* and the catalog generation advances only once EVERY required contribution has
|
|
8
|
+
* been accepted. A half-activated package is worse than an inactive one,
|
|
9
|
+
* because tools would appear that nothing can serve.
|
|
10
|
+
* - **Deactivate persists intent first.** The inactive preference is made durable
|
|
11
|
+
* BEFORE anything is withdrawn. If withdrawal then fails or the process dies,
|
|
12
|
+
* the durable answer is still "inactive", so a restart cannot resurrect a
|
|
13
|
+
* package the operator switched off.
|
|
14
|
+
*
|
|
15
|
+
* Rollback is deliberately NARROW: a failed activation withdraws only the handles
|
|
16
|
+
* it published itself. Touching another extension's contributions would turn one
|
|
17
|
+
* bad package into an outage of every package.
|
|
18
|
+
*
|
|
19
|
+
* @module @try-works/tool-mode-extension-sdk/install/lifecycle
|
|
20
|
+
*/
|
|
21
|
+
import { ManagementError } from "./../errors.js";
|
|
22
|
+
import { discoverExtension } from "./../discovery.js";
|
|
23
|
+
import { isProcessAlive } from "./store.js";
|
|
24
|
+
import { summarize } from "./../state.js";
|
|
25
|
+
const CALLABLE_READINESS = new Set([
|
|
26
|
+
'prepared-cold',
|
|
27
|
+
'loading',
|
|
28
|
+
'ready',
|
|
29
|
+
'busy',
|
|
30
|
+
]);
|
|
31
|
+
/** Default grace period for a contribution to finish stopping, in milliseconds. */
|
|
32
|
+
export const DEFAULT_STOP_GRACE_MS = 5000;
|
|
33
|
+
/**
|
|
34
|
+
* Whether an extension's readiness should block activation.
|
|
35
|
+
*
|
|
36
|
+
* EXT-26 is explicit that a valid package with merely UNPREPARED model assets may
|
|
37
|
+
* be active with `needs-preparation` diagnostics: the tools exist and can be
|
|
38
|
+
* described, and preparation happens on first use. A FAILED model is different —
|
|
39
|
+
* it will not become ready by waiting.
|
|
40
|
+
* @param readiness - per-deployment readiness.
|
|
41
|
+
* @returns diagnostics, and whether activation may proceed.
|
|
42
|
+
*/
|
|
43
|
+
export function assessReadiness(readiness) {
|
|
44
|
+
const diagnostics = [];
|
|
45
|
+
let proceed = true;
|
|
46
|
+
for (const [deployment, value] of Object.entries(readiness)) {
|
|
47
|
+
if (value === 'failed') {
|
|
48
|
+
proceed = false;
|
|
49
|
+
diagnostics.push({
|
|
50
|
+
code: 'not-registerable',
|
|
51
|
+
subject: deployment,
|
|
52
|
+
message: `the model deployment ${deployment} is in a failed state and will not become ready by waiting`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (value === 'needs-preparation' || value === 'preparing') {
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
code: 'needs-preparation',
|
|
58
|
+
subject: deployment,
|
|
59
|
+
message: `the model deployment ${deployment} is ${value}; working inference tools are withheld until preparation is complete`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { proceed, diagnostics };
|
|
64
|
+
}
|
|
65
|
+
/** The extension lifecycle for one profile. */
|
|
66
|
+
export class ExtensionLifecycle {
|
|
67
|
+
#store;
|
|
68
|
+
#profile;
|
|
69
|
+
#registrar;
|
|
70
|
+
#onWithdrawal;
|
|
71
|
+
#generation = 1;
|
|
72
|
+
#operations = new Set();
|
|
73
|
+
#assertIdle(extensionId) {
|
|
74
|
+
if (this.#operations.has(extensionId)) {
|
|
75
|
+
throw new ManagementError('EXTENSION_IN_USE', 'activate', `${extensionId} has a lifecycle operation in progress`, 'Wait for the current operation to settle, then retry.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async #exclusive(extensionId, work) {
|
|
79
|
+
this.#assertIdle(extensionId);
|
|
80
|
+
this.#operations.add(extensionId);
|
|
81
|
+
try {
|
|
82
|
+
return await work();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
this.#operations.delete(extensionId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Handles published by this profile, keyed by extension then contribution name. */
|
|
89
|
+
#published = new Map();
|
|
90
|
+
#callableTools = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Extensions whose published surface THIS instance owns.
|
|
93
|
+
*
|
|
94
|
+
* Distinct from `#published`, which is empty once every handle has been
|
|
95
|
+
* withdrawn. Reconciliation must not republish a surface another process owns:
|
|
96
|
+
* a CLI attached to no host holds no handles and cannot manage one.
|
|
97
|
+
*/
|
|
98
|
+
#owned = new Set();
|
|
99
|
+
constructor(options) {
|
|
100
|
+
this.#store = options.store;
|
|
101
|
+
this.#profile = options.profile;
|
|
102
|
+
this.#registrar = options.registrar;
|
|
103
|
+
this.#onWithdrawal = options.onWithdrawal;
|
|
104
|
+
this.#generation = options.generation ?? 1;
|
|
105
|
+
}
|
|
106
|
+
/** The current catalog generation. */
|
|
107
|
+
get generation() {
|
|
108
|
+
return this.#generation;
|
|
109
|
+
}
|
|
110
|
+
/** Read the persisted states for this profile. */
|
|
111
|
+
states() {
|
|
112
|
+
return this.#store.readProfileState(this.#profile).value;
|
|
113
|
+
}
|
|
114
|
+
/** Read one extension's persisted state. */
|
|
115
|
+
stateOf(extensionId) {
|
|
116
|
+
return this.states().find(state => state.extensionId === extensionId);
|
|
117
|
+
}
|
|
118
|
+
/** Replace one extension's state in the profile, preserving the others. */
|
|
119
|
+
#writeState(state) {
|
|
120
|
+
const others = this.states().filter(candidate => candidate.extensionId !== state.extensionId);
|
|
121
|
+
// The EXT-30 update pointers (activeVersions/previousVersions) live in the
|
|
122
|
+
// SAME profile document and are PRESERVED on every lifecycle write: the
|
|
123
|
+
// commit flow writes the pointer, and the rebind's own activate/deactivate
|
|
124
|
+
// must not erase it (the host's quiesce/rebind hooks call exactly these
|
|
125
|
+
// lifecycle writes). A corrupt pointer read fails closed like any other
|
|
126
|
+
// unreadable state — the write then proceeds WITHOUT inventing pointers.
|
|
127
|
+
const pointers = this.#store.readProfilePointers(this.#profile);
|
|
128
|
+
if (pointers.corrupt) {
|
|
129
|
+
this.#store.writeProfileState(this.#profile, [...others, state]);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const { activeVersions, previousVersions } = pointers.value;
|
|
133
|
+
this.#store.writeProfileState(this.#profile, [...others, state], undefined, activeVersions, previousVersions);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Activate an installed extension (EXT-26).
|
|
137
|
+
*
|
|
138
|
+
* Verifies the installed bytes and locks, checks readiness, publishes every
|
|
139
|
+
* required contribution, and advances the generation only if all of that
|
|
140
|
+
* succeeded. On any failure the contributions THIS call published are withdrawn
|
|
141
|
+
* and the generation is left where it was.
|
|
142
|
+
* @param extensionId - the extension to activate.
|
|
143
|
+
* @param version - the installed version to activate.
|
|
144
|
+
* @returns the activation outcome.
|
|
145
|
+
* @throws ManagementError when the extension is not installed, its content changed, or a contribution cannot be registered.
|
|
146
|
+
*/
|
|
147
|
+
async activate(extensionId, version) {
|
|
148
|
+
// The store lock wraps the WHOLE operation: activation writes state, and an
|
|
149
|
+
// offline CLI editing the same store must not interleave its read-modify-write
|
|
150
|
+
// between this one's read and write (EXT-31).
|
|
151
|
+
return this.#store.exclusive(() => this.#exclusive(extensionId, () => this.#activate(extensionId, version)));
|
|
152
|
+
}
|
|
153
|
+
async #activate(extensionId, version) {
|
|
154
|
+
const record = this.#findRecord(extensionId, version);
|
|
155
|
+
if (record === undefined) {
|
|
156
|
+
throw new ManagementError('EXTENSION_INACTIVE', 'activate', `${extensionId}@${version} is not installed`, 'Install it first; activation does not install.');
|
|
157
|
+
}
|
|
158
|
+
// Checked BEFORE anything is published: activating bytes that differ from the
|
|
159
|
+
// ones the operator approved would execute code nobody reviewed.
|
|
160
|
+
const integrity = this.#store.verifyUnchanged(record);
|
|
161
|
+
if (!integrity.unchanged) {
|
|
162
|
+
throw new ManagementError('INTEGRITY_MISMATCH', 'activate', `${extensionId}@${version} cannot be activated: ${integrity.diagnostic ?? 'the installed content changed'}`, 'Reinstall the package from its declared source, then activate it.');
|
|
163
|
+
}
|
|
164
|
+
const previous = this.stateOf(extensionId);
|
|
165
|
+
/**
|
|
166
|
+
* An extension THIS instance already has active is not a competing publication:
|
|
167
|
+
* re-activating it is how a caller picks up newly recorded readiness. Refusing
|
|
168
|
+
* here made preparation a dead end — every model-backed package publishes
|
|
169
|
+
* `implementation` metadata handles, so the guard fired even when nothing was
|
|
170
|
+
* actually serving a tool. Reconcile the surface instead.
|
|
171
|
+
*/
|
|
172
|
+
if (previous?.observedActivation === 'active' && this.#owned.has(extensionId)) {
|
|
173
|
+
const refresher = this.#store.listInstalled().value.find(candidate => candidate.extensionId === extensionId);
|
|
174
|
+
const refreshedTools = this.#toolsOf(record);
|
|
175
|
+
const refreshedReadiness = this.#readinessOf(record, previous.readiness);
|
|
176
|
+
const refreshedDiagnostics = [...assessReadiness(refreshedReadiness).diagnostics];
|
|
177
|
+
this.#writeState(this.#stateFor(record, {
|
|
178
|
+
desiredActivation: 'active',
|
|
179
|
+
observedActivation: 'active',
|
|
180
|
+
tools: refreshedTools,
|
|
181
|
+
readiness: refreshedReadiness,
|
|
182
|
+
}));
|
|
183
|
+
if (refresher !== undefined)
|
|
184
|
+
await this.#reconcileSurface(extensionId, refreshedDiagnostics);
|
|
185
|
+
return {
|
|
186
|
+
extensionId,
|
|
187
|
+
version,
|
|
188
|
+
state: this.stateOf(extensionId) ?? this.#stateFor(record, {
|
|
189
|
+
desiredActivation: 'active', observedActivation: 'active',
|
|
190
|
+
tools: refreshedTools, readiness: refreshedReadiness,
|
|
191
|
+
}),
|
|
192
|
+
diagnostics: refreshedDiagnostics,
|
|
193
|
+
generation: this.#generation,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* ADOPTION IS NOT ACTIVATION. State saying `active` means SOMEBODY published this
|
|
198
|
+
* surface. If this instance holds no handles for it, the question is whether that
|
|
199
|
+
* owner is GONE (a restart: restoring is the whole point of RUN-05) or ALIVE
|
|
200
|
+
* (another host: republishing would create a second owner serving the same names).
|
|
201
|
+
* The recorded owner PID is the only durable way to tell them apart, because
|
|
202
|
+
* handles are in-memory.
|
|
203
|
+
*
|
|
204
|
+
* The cases that must REFUSE are: a live foreign PID, and OUR OWN PID without
|
|
205
|
+
* local ownership (another instance of this process published it). A dead or
|
|
206
|
+
* absent PID is a restart of a legacy owner and may be restored.
|
|
207
|
+
*/
|
|
208
|
+
if (previous?.observedActivation === 'active' && !this.#owned.has(extensionId)) {
|
|
209
|
+
const pid = previous.ownerPid;
|
|
210
|
+
const foreignLive = pid !== undefined && pid !== process.pid && isProcessAlive(pid);
|
|
211
|
+
const sameProcessStranger = pid === process.pid;
|
|
212
|
+
if (foreignLive || sameProcessStranger) {
|
|
213
|
+
throw new ManagementError('EXTENSION_IN_USE', 'activate', `${extensionId} is already active under ${foreignLive ? `live process ${pid}` : 'another owner in this process'}`, 'Deactivate it through the owning host before activating it here.');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if ((this.#published.get(extensionId)?.size ?? 0) > 0
|
|
217
|
+
|| previous?.observedActivation === 'quarantined'
|
|
218
|
+
|| previous?.observedActivation === 'deactivating'
|
|
219
|
+
|| previous?.observedActivation === 'activating') {
|
|
220
|
+
throw new ManagementError('EXTENSION_IN_USE', 'activate', `${extensionId} is already published or requires cleanup before activation`, 'Deactivate through the owning host and confirm cleanup before activating again.');
|
|
221
|
+
}
|
|
222
|
+
const tools = this.#toolsOf(record);
|
|
223
|
+
const readiness = this.#readinessOf(record, previous?.readiness);
|
|
224
|
+
const assessment = assessReadiness(readiness);
|
|
225
|
+
const diagnostics = [...assessment.diagnostics];
|
|
226
|
+
if (!assessment.proceed) {
|
|
227
|
+
const reason = diagnostics.map(entry => entry.message).join('; ');
|
|
228
|
+
this.#writeState(this.#stateFor(record, {
|
|
229
|
+
desiredActivation: 'active',
|
|
230
|
+
observedActivation: 'failed',
|
|
231
|
+
tools, readiness,
|
|
232
|
+
}));
|
|
233
|
+
throw new ManagementError('EXTENSION_FAULT', 'activate', `${extensionId}@${version} cannot be activated: ${reason}`, 'Prepare or repair the model deployment, then activate again.');
|
|
234
|
+
}
|
|
235
|
+
// Intended-active is persisted before publication so a crash mid-activation
|
|
236
|
+
// leaves a record that activation was attempted, not a silent no-op.
|
|
237
|
+
this.#writeState(this.#stateFor(record, {
|
|
238
|
+
desiredActivation: 'active',
|
|
239
|
+
observedActivation: 'activating',
|
|
240
|
+
tools, readiness,
|
|
241
|
+
}));
|
|
242
|
+
const published = [];
|
|
243
|
+
const callableTools = new Set();
|
|
244
|
+
try {
|
|
245
|
+
for (const contribution of this.#contributionsOf(record, readiness, diagnostics)) {
|
|
246
|
+
if (contribution.kind === 'tool')
|
|
247
|
+
callableTools.add(contribution.name);
|
|
248
|
+
const handle = await this.#registrar.publish(contribution);
|
|
249
|
+
published.push({ kind: contribution.kind, name: contribution.name, handle });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
// Rollback is NARROW: only what this call published is withdrawn, so one bad
|
|
254
|
+
// package cannot disturb the others.
|
|
255
|
+
const remaining = new Map();
|
|
256
|
+
for (const entry of published.reverse()) {
|
|
257
|
+
try {
|
|
258
|
+
await this.#registrar.withdraw(entry.handle);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
remaining.set(entry.kind ? `${entry.kind}:${entry.name}` : `unknown:${entry.name}`, entry.handle);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (remaining.size > 0)
|
|
265
|
+
this.#published.set(extensionId, remaining);
|
|
266
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
267
|
+
this.#writeState(this.#stateFor(record, {
|
|
268
|
+
desiredActivation: 'active',
|
|
269
|
+
observedActivation: remaining.size > 0 ? 'quarantined' : 'failed',
|
|
270
|
+
tools, readiness,
|
|
271
|
+
}));
|
|
272
|
+
throw new ManagementError('EXTENSION_FAULT', 'activate', `${extensionId}@${version} was not activated: ${reason}`, 'Fix the reported contribution and activate again; other extensions were not disturbed.');
|
|
273
|
+
}
|
|
274
|
+
const handles = new Map(published.map(entry => [`${entry.kind}:${entry.name}`, entry.handle]));
|
|
275
|
+
this.#published.set(extensionId, handles);
|
|
276
|
+
this.#callableTools.set(extensionId, callableTools);
|
|
277
|
+
this.#owned.add(extensionId);
|
|
278
|
+
// Committed only now, once every required contribution was accepted.
|
|
279
|
+
this.#generation += 1;
|
|
280
|
+
const state = this.#stateFor(record, {
|
|
281
|
+
desiredActivation: 'active',
|
|
282
|
+
observedActivation: 'active',
|
|
283
|
+
tools, readiness,
|
|
284
|
+
});
|
|
285
|
+
this.#writeState(state);
|
|
286
|
+
return { extensionId, version, state, diagnostics, generation: this.#generation };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Deactivate an extension (EXT-27).
|
|
290
|
+
*
|
|
291
|
+
* The inactive intent is made durable FIRST, so a failure or crash during
|
|
292
|
+
* withdrawal cannot resurrect the package on the next start.
|
|
293
|
+
* @param extensionId - the extension to deactivate.
|
|
294
|
+
* @returns the deactivation outcome, which reports honestly when it is not fully stopped.
|
|
295
|
+
*/
|
|
296
|
+
async deactivate(extensionId) {
|
|
297
|
+
return this.#store.exclusive(() => this.#exclusive(extensionId, () => this.#deactivate(extensionId)));
|
|
298
|
+
}
|
|
299
|
+
async #deactivate(extensionId) {
|
|
300
|
+
const current = this.stateOf(extensionId);
|
|
301
|
+
if (current === undefined) {
|
|
302
|
+
throw new ManagementError('EXTENSION_INACTIVE', 'deactivate', `${extensionId} is not known in profile ${this.#profile}`, 'Nothing to deactivate.');
|
|
303
|
+
}
|
|
304
|
+
// ORDERING: persist inactive intent before withdrawing anything.
|
|
305
|
+
this.#writeState({ ...current, desiredActivation: 'inactive', observedActivation: 'deactivating' });
|
|
306
|
+
const handles = this.#published.get(extensionId) ?? new Map();
|
|
307
|
+
const hadHandles = handles.size > 0;
|
|
308
|
+
const failures = [];
|
|
309
|
+
for (const [name, handle] of handles) {
|
|
310
|
+
try {
|
|
311
|
+
await this.#registrar.withdraw(handle);
|
|
312
|
+
handles.delete(name);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (handles.size === 0) {
|
|
319
|
+
this.#published.delete(extensionId);
|
|
320
|
+
this.#callableTools.delete(extensionId);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* A store that says the extension was ACTIVE while this process holds no
|
|
324
|
+
* handles means somebody else published them — normally a host this CLI is not
|
|
325
|
+
* attached to, or a previous run that has since exited.
|
|
326
|
+
*
|
|
327
|
+
* Reporting a clean stop here would be a lie of exactly the kind EXT-27
|
|
328
|
+
* forbids: the contributions are still published somewhere, and this process
|
|
329
|
+
* has no way to withdraw them. Saying so is the only honest answer.
|
|
330
|
+
*/
|
|
331
|
+
const orphaned = (current.observedActivation === 'active' || current.observedActivation === 'quarantined'
|
|
332
|
+
|| current.observedActivation === 'deactivating' || current.observedActivation === 'activating') && !hadHandles;
|
|
333
|
+
if (orphaned) {
|
|
334
|
+
failures.push('the contributions were published by another process and cannot be withdrawn from here; '
|
|
335
|
+
+ 'stop the owning host, or deactivate through it');
|
|
336
|
+
}
|
|
337
|
+
const stopped = failures.length === 0;
|
|
338
|
+
/**
|
|
339
|
+
* onWithdrawal — a TEARDOWN OBSERVER, exactly once per withdrawn extension.
|
|
340
|
+
*
|
|
341
|
+
* It runs AFTER every registrar handle withdrawal has settled (successes
|
|
342
|
+
* and failures alike: a host must dispose its worker in both cases) and
|
|
343
|
+
* BEFORE the final state write, inside the existing store + operation
|
|
344
|
+
* locks. Its failure is RECORDED, never thrown: the state write below must
|
|
345
|
+
* commit even when a disposal callback misbehaves — losing the durable
|
|
346
|
+
* "inactive" answer to a hook error would let a restart resurrect the
|
|
347
|
+
* package the operator switched off. The error surfaces on the result.
|
|
348
|
+
*/
|
|
349
|
+
let hookFailure;
|
|
350
|
+
if (this.#onWithdrawal !== undefined) {
|
|
351
|
+
try {
|
|
352
|
+
await this.#onWithdrawal(extensionId);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
hookFailure = error instanceof Error ? error.message : String(error);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const { ownerPid: _previousOwner, ...withoutOwner } = current;
|
|
359
|
+
const inactive = {
|
|
360
|
+
...(stopped ? withoutOwner : current),
|
|
361
|
+
desiredActivation: 'inactive',
|
|
362
|
+
observedActivation: stopped ? 'inactive' : 'quarantined',
|
|
363
|
+
};
|
|
364
|
+
this.#writeState(inactive);
|
|
365
|
+
// Ownership ends only with a CONFIRMED stop; a quarantined extension still has
|
|
366
|
+
// live handlers this instance must be able to retry withdrawing.
|
|
367
|
+
if (failures.length === 0) {
|
|
368
|
+
this.#generation += 1;
|
|
369
|
+
this.#owned.delete(extensionId);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
extensionId,
|
|
373
|
+
state: inactive,
|
|
374
|
+
// Never claim a full stop while something it owns is still running.
|
|
375
|
+
fullyStopped: failures.length === 0,
|
|
376
|
+
...failures.length === 0 ? {} : { cleanupFailure: failures.join('; ') },
|
|
377
|
+
...hookFailure === undefined ? {} : { hookFailure },
|
|
378
|
+
generation: this.#generation,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Enable or disable ONE tool inside an active extension (EXT-04).
|
|
383
|
+
*
|
|
384
|
+
* Independent of activation: the package stays active while a single tool is
|
|
385
|
+
* hidden from callers.
|
|
386
|
+
*
|
|
387
|
+
* ASYNC because the toggle also reconciles the PUBLISHED surface: a disabled
|
|
388
|
+
* tool is withdrawn from the registrar rather than merely remembered as off,
|
|
389
|
+
* and a re-enabled one is published again. When this instance does not own the
|
|
390
|
+
* surface — a CLI with no host attached — only the durable choice is recorded,
|
|
391
|
+
* because it cannot manage a registry another process owns.
|
|
392
|
+
* @param extensionId - the owning extension.
|
|
393
|
+
* @param toolName - the tool to toggle.
|
|
394
|
+
* @param enabled - the new visibility.
|
|
395
|
+
* @throws ManagementError when the extension or tool is unknown.
|
|
396
|
+
* @returns the state after the change.
|
|
397
|
+
*/
|
|
398
|
+
async setToolEnabled(extensionId, toolName, enabled) {
|
|
399
|
+
return this.#store.exclusive(() => this.#exclusive(extensionId, () => this.#setToolEnabled(extensionId, toolName, enabled)));
|
|
400
|
+
}
|
|
401
|
+
async #setToolEnabled(extensionId, toolName, enabled) {
|
|
402
|
+
const current = this.stateOf(extensionId);
|
|
403
|
+
if (current === undefined) {
|
|
404
|
+
throw new ManagementError('EXTENSION_INACTIVE', 'deactivate', `${extensionId} is not known`, 'Nothing to toggle.');
|
|
405
|
+
}
|
|
406
|
+
const tool = current.tools.find(candidate => candidate.toolName === toolName);
|
|
407
|
+
if (tool === undefined) {
|
|
408
|
+
throw new ManagementError('INVALID_EXTENSION', 'deactivate', `${extensionId} contributes no tool named ${JSON.stringify(toolName)}`, 'List the extension to see the tools it contributes.');
|
|
409
|
+
}
|
|
410
|
+
const next = {
|
|
411
|
+
...current,
|
|
412
|
+
tools: current.tools.map(candidate => candidate.toolName === toolName ? { ...candidate, enabled } : candidate),
|
|
413
|
+
};
|
|
414
|
+
/**
|
|
415
|
+
* INTENT IS PERSISTED BEFORE THE SURFACE MOVES. Disabling a tool that stays
|
|
416
|
+
* REGISTERED would keep answering callers, which is the defect this ordering
|
|
417
|
+
* exists to close; writing the choice first means a crash mid-reconcile
|
|
418
|
+
* leaves the durable answer as "off" rather than silently "on".
|
|
419
|
+
*/
|
|
420
|
+
this.#writeState(next);
|
|
421
|
+
if (next.observedActivation === 'active' && this.#owned.has(extensionId)) {
|
|
422
|
+
await this.#reconcileSurface(extensionId);
|
|
423
|
+
}
|
|
424
|
+
return this.stateOf(extensionId) ?? next;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Bring the PUBLISHED surface back in line with the current state (EXT-04).
|
|
428
|
+
*
|
|
429
|
+
* A tool toggle is not only a state edit: the registrar still holds the handles
|
|
430
|
+
* from activation. This recomputes the desired contribution set — which already
|
|
431
|
+
* encodes disabled tools, readiness gates and dependent usage skills — and then
|
|
432
|
+
* withdraws what is no longer wanted and publishes what is newly wanted.
|
|
433
|
+
*
|
|
434
|
+
* Nothing is recomputed for an extension that is not active: a deactivated
|
|
435
|
+
* package has no visible surface to reconcile, so this is a no-op there.
|
|
436
|
+
* @param extensionId - the extension whose surface to reconcile.
|
|
437
|
+
*/
|
|
438
|
+
async #reconcileSurface(extensionId, diagnostics = []) {
|
|
439
|
+
const record = this.#store.listInstalled().value.find(candidate => candidate.extensionId === extensionId);
|
|
440
|
+
if (record === undefined)
|
|
441
|
+
return;
|
|
442
|
+
/**
|
|
443
|
+
* THE SAME INTEGRITY GATE ACTIVATION USES. Activation refuses to publish bytes
|
|
444
|
+
* that changed after installation; reconcile publishes and withdraws through the
|
|
445
|
+
* SAME registry, so skipping the check here would make "edit the installed
|
|
446
|
+
* package, then toggle an unrelated tool" a way to run code nobody reviewed.
|
|
447
|
+
*/
|
|
448
|
+
const integrity = this.#store.verifyUnchanged(record);
|
|
449
|
+
if (!integrity.unchanged) {
|
|
450
|
+
throw new ManagementError('INTEGRITY_MISMATCH', 'activate', `${extensionId} surface cannot be reconciled: ${integrity.diagnostic ?? 'the installed content changed'}`, 'Reinstall the package from its declared source, then toggle the tool.');
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* AN UNKNOWN DESIRED SET IS NOT AN EMPTY ONE. `#contributionsOf` returns [] when
|
|
454
|
+
* the manifest cannot be read, and treating that as "nothing is wanted" would
|
|
455
|
+
* withdraw the ENTIRE surface — a transient AV-scanner lock or concurrent writer
|
|
456
|
+
* would silently unregister every tool while the durable state still said active.
|
|
457
|
+
* Refuse instead, leaving the surface exactly as it was.
|
|
458
|
+
*/
|
|
459
|
+
const discovery = this.#discoverOf(record);
|
|
460
|
+
if (discovery === undefined) {
|
|
461
|
+
throw new ManagementError('INVALID_EXTENSION', 'activate', `${extensionId} manifest could not be read, so the published surface was left untouched`, 'Ensure the installed package is readable, then retry.');
|
|
462
|
+
}
|
|
463
|
+
const state = this.stateOf(extensionId);
|
|
464
|
+
const previous = this.#published.get(extensionId) ?? new Map();
|
|
465
|
+
const desired = this.#contributionsOf(record, state?.readiness ?? {}, diagnostics);
|
|
466
|
+
/**
|
|
467
|
+
* Keyed by KIND AND NAME. A manifest may legally declare a tool named `shared`
|
|
468
|
+
* and an implementation whose id is also `shared` (uniqueness is per-label), so
|
|
469
|
+
* name-only keying would let one kind's survival mask the other's withdrawal.
|
|
470
|
+
*/
|
|
471
|
+
const keyOf = (contribution) => `${contribution.kind}:${contribution.name}`;
|
|
472
|
+
const desiredKeys = new Set(desired.map(keyOf));
|
|
473
|
+
/**
|
|
474
|
+
* A REDUNDANT TOGGLE IS A NO-OP. When the published keys already equal the
|
|
475
|
+
* desired keys, there is nothing to withdraw or publish, and bumping the
|
|
476
|
+
* generation would invalidate consumer caches for a surface that did not move.
|
|
477
|
+
* This check ALSO makes a retry cheap: after a failed withdrawal the surviving
|
|
478
|
+
* key is still present, so a retry that now succeeds is a real change and does
|
|
479
|
+
* move the generation.
|
|
480
|
+
*/
|
|
481
|
+
const existingKeys = new Set(previous.keys());
|
|
482
|
+
if (existingKeys.size === desiredKeys.size && [...existingKeys].every(key => desiredKeys.has(key))) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* `live` and `visible` record what ACTUALLY exists after this pass, and they are
|
|
487
|
+
* committed in the `finally` below. Mutating the stored map in place and only then
|
|
488
|
+
* updating `#callableTools` would let a mid-pass failure leave admission asserting
|
|
489
|
+
* a surface the registry no longer has — the exact divergence this reconcile
|
|
490
|
+
* exists to remove.
|
|
491
|
+
*/
|
|
492
|
+
const live = new Map();
|
|
493
|
+
const visible = new Set();
|
|
494
|
+
const failures = [];
|
|
495
|
+
for (const [key, handle] of [...previous]) {
|
|
496
|
+
// Keys are `kind:name`, so the KIND participates in the match rather than
|
|
497
|
+
// only the name: a tool and an implementation may legally share a name.
|
|
498
|
+
const name = key.slice(key.indexOf(':') + 1);
|
|
499
|
+
if (desiredKeys.has(key)) {
|
|
500
|
+
live.set(key, handle);
|
|
501
|
+
visible.add(name);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
await this.#registrar.withdraw(handle);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
// A handle that would not withdraw is STILL PUBLISHED, so it stays tracked
|
|
509
|
+
// and stays visible; forgetting it would be the lie.
|
|
510
|
+
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
511
|
+
live.set(key, handle);
|
|
512
|
+
visible.add(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const contribution of desired) {
|
|
516
|
+
const key = keyOf(contribution);
|
|
517
|
+
if (live.has(key))
|
|
518
|
+
continue;
|
|
519
|
+
try {
|
|
520
|
+
const handle = await this.#registrar.publish(contribution);
|
|
521
|
+
live.set(key, handle);
|
|
522
|
+
visible.add(contribution.name);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
failures.push(`${contribution.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (live.size === 0)
|
|
529
|
+
this.#published.delete(extensionId);
|
|
530
|
+
else
|
|
531
|
+
this.#published.set(extensionId, live);
|
|
532
|
+
this.#callableTools.set(extensionId, new Set(desired.filter(contribution => contribution.kind === 'tool' && visible.has(contribution.name))
|
|
533
|
+
.map(contribution => contribution.name)));
|
|
534
|
+
/**
|
|
535
|
+
* The surface moved (fully or partly), so consumers must see a new generation.
|
|
536
|
+
* This advances BEFORE the failure is raised, because a partly-reconciled
|
|
537
|
+
* surface is still a different surface and must not be served under the old
|
|
538
|
+
* generation.
|
|
539
|
+
*/
|
|
540
|
+
this.#generation += 1;
|
|
541
|
+
if (failures.length > 0) {
|
|
542
|
+
throw new ManagementError('EXTENSION_FAULT', 'deactivate', `${extensionId} surface was only partly reconciled: ${failures.join('; ')}`, 'Retry the toggle once the reported contribution can be managed; what remains published is still tracked.');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Whether a tool may be invoked right now.
|
|
547
|
+
* @param extensionId - the owning extension.
|
|
548
|
+
* @param toolName - the tool being invoked.
|
|
549
|
+
*/
|
|
550
|
+
admits(extensionId, toolName) {
|
|
551
|
+
const current = this.stateOf(extensionId);
|
|
552
|
+
if (current === undefined)
|
|
553
|
+
return false;
|
|
554
|
+
/**
|
|
555
|
+
* ADMISSION MUST AGREE WITH PUBLICATION. The surface is exactly the tool
|
|
556
|
+
* contributions this instance committed, so a tool withheld for a missing,
|
|
557
|
+
* mismatched or unprepared binding is not admitted merely because its
|
|
558
|
+
* enablement flag survives in state. Readiness is already folded into that
|
|
559
|
+
* committed set, which also stops an UNRELATED deployment's preparation from
|
|
560
|
+
* rejecting a tool whose own binding is callable.
|
|
561
|
+
*/
|
|
562
|
+
if (!(this.#callableTools.get(extensionId)?.has(toolName) ?? false))
|
|
563
|
+
return false;
|
|
564
|
+
/**
|
|
565
|
+
* Enablement and lifecycle are checked HERE rather than through
|
|
566
|
+
* `admitsInvocation`, because that helper also requires EVERY declared
|
|
567
|
+
* deployment to be callable. This tool's binding was already evaluated when its
|
|
568
|
+
* contribution was published, so an unrelated unprepared deployment must not
|
|
569
|
+
* make a callable tool inadmissible.
|
|
570
|
+
*/
|
|
571
|
+
if (current.installation !== 'installed')
|
|
572
|
+
return false;
|
|
573
|
+
if (current.desiredActivation !== 'active' || current.observedActivation !== 'active')
|
|
574
|
+
return false;
|
|
575
|
+
return current.tools.find(tool => tool.toolName === toolName)?.enabled === true;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Uninstall an installed extension (EXT-04, EXT-28).
|
|
579
|
+
* @param extensionId - the extension to remove.
|
|
580
|
+
* @param options - `deactivate` must be set to remove an active package.
|
|
581
|
+
* @throws ManagementError `EXTENSION_IN_USE`, `BUILTIN_PROTECTED`, or `EXTENSION_INACTIVE`.
|
|
582
|
+
*/
|
|
583
|
+
async uninstall(extensionId, options = {}) {
|
|
584
|
+
return this.#store.exclusive(() => this.#exclusive(extensionId, () => this.#uninstall(extensionId, options)));
|
|
585
|
+
}
|
|
586
|
+
async #uninstall(extensionId, options) {
|
|
587
|
+
const current = this.stateOf(extensionId);
|
|
588
|
+
const active = current?.observedActivation === 'active' || current?.desiredActivation === 'active'
|
|
589
|
+
|| current?.observedActivation === 'quarantined' || current?.observedActivation === 'deactivating'
|
|
590
|
+
|| (this.#published.get(extensionId)?.size ?? 0) > 0;
|
|
591
|
+
if (active && options.deactivate !== true) {
|
|
592
|
+
throw new ManagementError('EXTENSION_IN_USE', 'uninstall', `${extensionId} is active in profile ${this.#profile}`, 'Re-run with deactivation requested; the affected profiles and tools are listed in the preview.');
|
|
593
|
+
}
|
|
594
|
+
if (active) {
|
|
595
|
+
const stopped = await this.#deactivate(extensionId);
|
|
596
|
+
if (!stopped.fullyStopped) {
|
|
597
|
+
throw new ManagementError('EXTENSION_IN_USE', 'uninstall', `${extensionId} cleanup is incomplete: ${stopped.cleanupFailure ?? 'owned resources are not confirmed stopped'}`, 'Retry cleanup through the owning host; installed bytes and quarantine are retained.');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// The disabled tombstone is RETAINED rather than deleted, so a later automatic
|
|
601
|
+
// install cannot silently resurrect a package the operator removed (EXT-28).
|
|
602
|
+
const previous = this.stateOf(extensionId);
|
|
603
|
+
const { ownerPid: _staleOwner, ...withoutOwner } = previous ?? {
|
|
604
|
+
extensionId,
|
|
605
|
+
installation: 'absent',
|
|
606
|
+
desiredActivation: 'inactive',
|
|
607
|
+
observedActivation: 'inactive',
|
|
608
|
+
readiness: {},
|
|
609
|
+
tools: [],
|
|
610
|
+
export: 'not-exported',
|
|
611
|
+
};
|
|
612
|
+
this.#writeState({
|
|
613
|
+
...withoutOwner,
|
|
614
|
+
installation: 'absent',
|
|
615
|
+
desiredActivation: 'inactive',
|
|
616
|
+
observedActivation: 'inactive',
|
|
617
|
+
tools: [],
|
|
618
|
+
});
|
|
619
|
+
this.#callableTools.delete(extensionId);
|
|
620
|
+
this.#owned.delete(extensionId);
|
|
621
|
+
this.#store.removeRecord(extensionId);
|
|
622
|
+
}
|
|
623
|
+
#findRecord(extensionId, version) {
|
|
624
|
+
return this.#store.listInstalled().value.find(record => record.extensionId === extensionId && record.version === version);
|
|
625
|
+
}
|
|
626
|
+
/** The tools a record contributes, all enabled by default. */
|
|
627
|
+
#toolsOf(record) {
|
|
628
|
+
const discovery = this.#discoverOf(record);
|
|
629
|
+
const tools = discovery?.manifest.tools ?? [];
|
|
630
|
+
const previous = this.stateOf(record.extensionId)?.tools ?? [];
|
|
631
|
+
return tools.map(tool => {
|
|
632
|
+
const previousTool = previous.find(candidate => candidate.toolName === tool.name);
|
|
633
|
+
return {
|
|
634
|
+
toolName: tool.name,
|
|
635
|
+
enabled: previousTool?.enabled ?? true,
|
|
636
|
+
// A preserved provider pick must survive re-activation too: rebuilding
|
|
637
|
+
// the selection from the manifest drops it otherwise, which would undo
|
|
638
|
+
// the BASE-09 migration's provider preservation on the next refresh.
|
|
639
|
+
...previousTool?.selectedImplementation === undefined ? {} : { selectedImplementation: previousTool.selectedImplementation },
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/** Re-read the installed manifest, or undefined when it cannot be read. */
|
|
644
|
+
#discoverOf(record) {
|
|
645
|
+
try {
|
|
646
|
+
return discoverExtension(record.installPath);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
// A package whose manifest no longer reads is reported by the caller as
|
|
650
|
+
// contributing nothing, rather than crashing the whole lifecycle.
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** Default declared model deployments to needs-preparation until a host records a stronger fact. */
|
|
655
|
+
#readinessOf(record, previous) {
|
|
656
|
+
const discovery = this.#discoverOf(record);
|
|
657
|
+
const readiness = {};
|
|
658
|
+
for (const model of discovery?.manifest.models ?? []) {
|
|
659
|
+
readiness[model.id] = previous?.[model.id] ?? 'needs-preparation';
|
|
660
|
+
}
|
|
661
|
+
return readiness;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Record host-inspected readiness for an extension's declared deployments.
|
|
665
|
+
*
|
|
666
|
+
* This is the production writer `#readinessOf` reads: without it, every declared
|
|
667
|
+
* model stays `needs-preparation` forever and a model-backed package can never
|
|
668
|
+
* publish a callable tool. Inspection is the host's read-only EXT-18 surface, so
|
|
669
|
+
* recording it here is how "prepared" becomes a durable fact rather than a guess.
|
|
670
|
+
*
|
|
671
|
+
* Only values for deployments the CURRENT manifest declares are kept, matching the
|
|
672
|
+
* projection `#readinessOf` already applies, and `failed` is recorded so a failed
|
|
673
|
+
* deployment refuses activation instead of staying silent.
|
|
674
|
+
*
|
|
675
|
+
* ASYNC because a readiness CHANGE also moves the surface: a deployment that just
|
|
676
|
+
* became `failed` must have its tool withdrawn, and one that just became callable
|
|
677
|
+
* must be published. Recording without reconciling would leave the published
|
|
678
|
+
* snapshot stale, which is exactly the divergence admission exists to prevent.
|
|
679
|
+
* @param extensionId - the extension whose deployments were inspected.
|
|
680
|
+
* @param inspected - readiness keyed by deployment id.
|
|
681
|
+
* @returns the state after recording.
|
|
682
|
+
* @throws ManagementError when the extension is not installed.
|
|
683
|
+
*/
|
|
684
|
+
async recordReadiness(extensionId, inspected) {
|
|
685
|
+
return this.#store.exclusive(() => this.#exclusive(extensionId, () => this.#recordReadiness(extensionId, inspected)));
|
|
686
|
+
}
|
|
687
|
+
async #recordReadiness(extensionId, inspected) {
|
|
688
|
+
const current = this.stateOf(extensionId);
|
|
689
|
+
if (current === undefined) {
|
|
690
|
+
throw new ManagementError('EXTENSION_INACTIVE', 'activate', `${extensionId} is not known`, 'Nothing to record for it.');
|
|
691
|
+
}
|
|
692
|
+
const record = this.#store.listInstalled().value.find(candidate => candidate.extensionId === extensionId);
|
|
693
|
+
if (record === undefined) {
|
|
694
|
+
throw new ManagementError('EXTENSION_INACTIVE', 'activate', `${extensionId} is not installed`, 'Install it before recording readiness.');
|
|
695
|
+
}
|
|
696
|
+
const discovery = this.#discoverOf(record);
|
|
697
|
+
if (discovery === undefined) {
|
|
698
|
+
// An UNREADABLE manifest cannot say which deployments are declared, so recording
|
|
699
|
+
// against it would write junk into the durable state. Refuse rather than guess.
|
|
700
|
+
throw new ManagementError('INVALID_EXTENSION', 'activate', `${extensionId} manifest could not be read, so readiness was not recorded`, 'Ensure the installed package is readable, then retry.');
|
|
701
|
+
}
|
|
702
|
+
const declared = new Set(discovery.manifest.models.map(model => model.id));
|
|
703
|
+
const readiness = { ...current.readiness };
|
|
704
|
+
for (const [id, value] of Object.entries(inspected)) {
|
|
705
|
+
// Unknown deployments are DROPPED, mirroring the projection activation applies:
|
|
706
|
+
// a removed deployment's stale readiness must not poison the current manifest.
|
|
707
|
+
if (declared.has(id))
|
|
708
|
+
readiness[id] = value;
|
|
709
|
+
}
|
|
710
|
+
const changed = Object.entries(readiness).some(([id, value]) => current.readiness[id] !== value);
|
|
711
|
+
const next = { ...current, readiness };
|
|
712
|
+
this.#writeState(next);
|
|
713
|
+
// The surface must agree with the new facts: a tool whose deployment just failed
|
|
714
|
+
// is withdrawn, and one that just became callable is published.
|
|
715
|
+
if (changed && next.observedActivation === 'active' && this.#owned.has(extensionId)) {
|
|
716
|
+
await this.#reconcileSurface(extensionId);
|
|
717
|
+
}
|
|
718
|
+
return this.stateOf(extensionId) ?? next;
|
|
719
|
+
}
|
|
720
|
+
/** The contributions a record publishes. */
|
|
721
|
+
#contributionsOf(record, readiness, diagnostics) {
|
|
722
|
+
const discovery = this.#discoverOf(record);
|
|
723
|
+
if (discovery === undefined)
|
|
724
|
+
return [];
|
|
725
|
+
const contributions = [];
|
|
726
|
+
const disabled = new Set(this.stateOf(record.extensionId)?.tools
|
|
727
|
+
.filter(tool => !tool.enabled).map(tool => tool.toolName) ?? []);
|
|
728
|
+
for (const tool of discovery.manifest.tools) {
|
|
729
|
+
if (disabled.has(tool.name))
|
|
730
|
+
continue;
|
|
731
|
+
const candidates = discovery.manifest.implementations.filter(implementation => implementation.contractId === tool.contractId);
|
|
732
|
+
const implementations = candidates.filter(implementation => implementation.contractVersion === tool.contractVersion);
|
|
733
|
+
if (implementations.length === 0) {
|
|
734
|
+
diagnostics.push({
|
|
735
|
+
code: 'not-registerable',
|
|
736
|
+
subject: tool.name,
|
|
737
|
+
message: `${tool.name} is withheld because no implementation serves ${tool.contractId}@${tool.contractVersion}`,
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
// Dispatch currently selects the FIRST exact contract binding. Do not let
|
|
742
|
+
// a later ready alternative justify visibility for that unprepared target.
|
|
743
|
+
const selected = implementations[0];
|
|
744
|
+
const callable = selected?.modelId === undefined
|
|
745
|
+
|| CALLABLE_READINESS.has(readiness[selected.modelId] ?? 'needs-preparation');
|
|
746
|
+
if (!callable) {
|
|
747
|
+
diagnostics.push({
|
|
748
|
+
code: 'needs-preparation',
|
|
749
|
+
subject: tool.name,
|
|
750
|
+
message: `${tool.name} is withheld because no implementation for ${tool.contractId} is prepared and callable`,
|
|
751
|
+
});
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
contributions.push({
|
|
755
|
+
extensionId: record.extensionId, kind: 'tool', name: tool.name, contractId: tool.contractId,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
for (const implementation of discovery.manifest.implementations) {
|
|
759
|
+
contributions.push({
|
|
760
|
+
extensionId: record.extensionId,
|
|
761
|
+
kind: 'implementation',
|
|
762
|
+
name: implementation.id,
|
|
763
|
+
contractId: implementation.contractId,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const visibleTools = new Set(contributions.filter(entry => entry.kind === 'tool').map(entry => entry.name));
|
|
767
|
+
for (const skill of discovery.manifest.skills) {
|
|
768
|
+
// Setup guidance has no required tools; usage guidance must not advertise
|
|
769
|
+
// leaves withheld by readiness. Keep both projections in the same decision.
|
|
770
|
+
if (skill.requiresTools?.some(id => {
|
|
771
|
+
const tool = discovery.manifest.tools.find(candidate => candidate.id === id);
|
|
772
|
+
return !visibleTools.has(tool?.name ?? id);
|
|
773
|
+
}))
|
|
774
|
+
continue;
|
|
775
|
+
contributions.push({ extensionId: record.extensionId, kind: 'skill', name: skill.name });
|
|
776
|
+
}
|
|
777
|
+
return contributions;
|
|
778
|
+
}
|
|
779
|
+
/** Build a state value for a record. */
|
|
780
|
+
#stateFor(record, overrides) {
|
|
781
|
+
const previous = this.stateOf(record.extensionId);
|
|
782
|
+
return {
|
|
783
|
+
extensionId: record.extensionId,
|
|
784
|
+
installation: 'installed',
|
|
785
|
+
export: previous?.export ?? 'not-exported',
|
|
786
|
+
/**
|
|
787
|
+
* The PID is stamped while THIS process publishes or is about to publish the
|
|
788
|
+
* surface, and dropped on any confirmed stop. That is what lets a restarted
|
|
789
|
+
* process restore an `active` record whose owner died, while still refusing a
|
|
790
|
+
* LIVE second owner.
|
|
791
|
+
*/
|
|
792
|
+
...overrides.observedActivation === 'active' || overrides.observedActivation === 'activating'
|
|
793
|
+
? { ownerPid: process.pid }
|
|
794
|
+
: {},
|
|
795
|
+
...overrides,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
export { summarize };
|
|
800
|
+
//# sourceMappingURL=lifecycle.js.map
|