@hasna/skills 0.5.6 → 0.5.8

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/README.md CHANGED
@@ -522,7 +522,10 @@ the same declaration, and an uncertain upload is never sent twice. If a process
522
522
  crashes while holding `operation.lock`, confirm it has stopped before removing
523
523
  that lock explicitly. Status and cancellation remain available when new
524
524
  publishing is disabled. Exit 2 means publication is still pending; `committed`
525
- means source was published. Private execution remains unavailable.
525
+ means source was published. Execution requires a separate server quote and approval.
526
+ Publication recovery results report `executionEnabled: null` because their durable
527
+ receipts contain no server capability observation. Use `getCapability()` for the
528
+ server's current boolean capability; it does not authorize an individual run.
526
529
 
527
530
  The SDK exports `RemotePrivatePublicationsClient` through both the root and
528
531
  `./sdk`; `RemoteSkillsAuthClient.openPrivatePublications` creates one from fresh
@@ -1193,3 +1196,46 @@ returned or saved by the tools.
1193
1196
 
1194
1197
  These clients still require deployed configuration and controlled real recipient
1195
1198
  email acceptance before recovery can be offered as a live product capability.
1199
+
1200
+ ### Injected operation transport
1201
+
1202
+ `@hasna/skills/sdk` exports `createSkillOperationClient` for an embedder-supplied
1203
+ `SkillOperationTransport`. It provides a bounded JSON envelope, immutable
1204
+ snapshots and explicit status lookup. It does not connect to a provider, discover
1205
+ an endpoint, read credentials, or provide guest IPC or authorization.
1206
+
1207
+ ```ts
1208
+ import { createSkillOperationClient, type SkillOperationTransport } from "@hasna/skills/sdk";
1209
+
1210
+ function operationsForCapturedRun(transport: SkillOperationTransport) {
1211
+ return createSkillOperationClient(transport, { timeoutMs: 30_000 });
1212
+ }
1213
+ // The embedder supplies invoke(request, { signal }) and get(requestId, { signal }).
1214
+ // invoke accepts { contractVersion: 1, requestId: UUID, operation: "text.generate",
1215
+ // input: { prompt: "..." } }.
1216
+ ```
1217
+
1218
+ Create one client per captured authority scope. The transport must enforce that
1219
+ scope, bind the request ID to the exact payload durably, and enforce approval,
1220
+ budget and execution policy. The client remembers up to 256 request identities
1221
+ and 1 MiB of canonical requests. It refuses capacity before transport and never
1222
+ evicts an old identity. An explicit repeat with the same payload makes one new
1223
+ transport call; a changed payload under a remembered ID is refused locally.
1224
+ This local check does not replace server deduplication.
1225
+
1226
+ Requests are limited to 64 KiB and responses to 1 MiB of serialized UTF-8 JSON,
1227
+ with depth and node limits exported in `SKILL_OPERATION_LIMITS`. Plain JSON data
1228
+ is copied and deeply frozen; cycles, accessors, `toJSON` functions, unsupported
1229
+ values and extra envelope fields are refused. Ordinary data inside `input` and
1230
+ `output` is preserved. Request IDs use canonical lowercase UUID strings;
1231
+ operation names use lowercase letters, digits and dot or hyphen separators.
1232
+
1233
+ Responses preserve `contractVersion` and `requestId`. A status of `succeeded`
1234
+ includes `output`; `refused` includes a fixed `SkillOperationRefusal` code.
1235
+ `pending`, `unknown` and authoritative `not-executed` have no additional fields.
1236
+ Transport failure, in-flight abort and timeout produce a safe
1237
+ `SkillOperationClientError` with an unknown outcome. They never prove that an
1238
+ operation did not execute. The client does not retry or issue a status read
1239
+ automatically: explicitly call `get` with the same request ID to reconcile.
1240
+ Aborting locally does not establish server cancellation. An already-aborted
1241
+ signal refuses before calling the transport.
package/bin/index.js CHANGED
@@ -36860,7 +36860,7 @@ var package_default;
36860
36860
  var init_package = __esm(() => {
36861
36861
  package_default = {
36862
36862
  name: "@hasna/skills",
36863
- version: "0.5.6",
36863
+ version: "0.5.8",
36864
36864
  description: "Skills library for AI coding agents",
36865
36865
  type: "module",
36866
36866
  bin: {
@@ -72952,7 +72952,7 @@ var init_mcp_contracts = __esm(() => {
72952
72952
  ].map((operation) => ({
72953
72953
  name: operation.name,
72954
72954
  title: operation.title,
72955
- description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; private execution remains unavailable.",
72955
+ description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; execution requires a separate server quote and approval.",
72956
72956
  params: [...Object.keys(publicationVerification), ...Object.keys(operation.extras)],
72957
72957
  category: "storage",
72958
72958
  sideEffects: "filesystem",
@@ -72965,7 +72965,7 @@ var init_mcp_contracts = __esm(() => {
72965
72965
  state: { type: "string" },
72966
72966
  versionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
72967
72967
  committed: { type: "boolean" },
72968
- executionEnabled: { const: false },
72968
+ executionEnabled: { oneOf: [{ type: "boolean" }, { type: "null" }] },
72969
72969
  nextAction: { type: "string" }
72970
72970
  }, ["recoveryDirectory", "skillId", "intentId", "state", "versionId", "committed", "executionEnabled", "nextAction"])
72971
72971
  }));
@@ -74747,7 +74747,7 @@ class RemotePrivatePublicationsClient {
74747
74747
  return bad();
74748
74748
  const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
74749
74749
  const p = record7(response) && response.privatePublishing;
74750
- if (!record7(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || p.executionEnabled !== false)
74750
+ if (!record7(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || typeof p.executionEnabled !== "boolean")
74751
74751
  throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
74752
74752
  return Object.freeze({ ...p });
74753
74753
  }
@@ -75655,7 +75655,7 @@ async function preparePrivatePublication(client, sourceDirectory, recoveryDirect
75655
75655
  }
75656
75656
  function privatePublicationResult(directory, receipt) {
75657
75657
  const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
75658
- const nextAction = committed ? "The version is published. Private execution remains unavailable." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
75658
+ const nextAction = committed ? "Published; execution requires a separate server quote and approval." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
75659
75659
  return {
75660
75660
  recoveryDirectory: directory,
75661
75661
  skillId: receipt.skillId,
@@ -75663,7 +75663,7 @@ function privatePublicationResult(directory, receipt) {
75663
75663
  state,
75664
75664
  versionId: receipt.intent?.versionId ?? null,
75665
75665
  committed,
75666
- executionEnabled: false,
75666
+ executionEnabled: null,
75667
75667
  nextAction
75668
75668
  };
75669
75669
  }
package/bin/mcp.js CHANGED
@@ -5586,7 +5586,7 @@ var package_default;
5586
5586
  var init_package = __esm(() => {
5587
5587
  package_default = {
5588
5588
  name: "@hasna/skills",
5589
- version: "0.5.6",
5589
+ version: "0.5.8",
5590
5590
  description: "Skills library for AI coding agents",
5591
5591
  type: "module",
5592
5592
  bin: {
@@ -26861,7 +26861,7 @@ var init_mcp_contracts = __esm(() => {
26861
26861
  ].map((operation) => ({
26862
26862
  name: operation.name,
26863
26863
  title: operation.title,
26864
- description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; private execution remains unavailable.",
26864
+ description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; execution requires a separate server quote and approval.",
26865
26865
  params: [...Object.keys(publicationVerification), ...Object.keys(operation.extras)],
26866
26866
  category: "storage",
26867
26867
  sideEffects: "filesystem",
@@ -26874,7 +26874,7 @@ var init_mcp_contracts = __esm(() => {
26874
26874
  state: { type: "string" },
26875
26875
  versionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
26876
26876
  committed: { type: "boolean" },
26877
- executionEnabled: { const: false },
26877
+ executionEnabled: { oneOf: [{ type: "boolean" }, { type: "null" }] },
26878
26878
  nextAction: { type: "string" }
26879
26879
  }, ["recoveryDirectory", "skillId", "intentId", "state", "versionId", "committed", "executionEnabled", "nextAction"])
26880
26880
  }));
@@ -31000,7 +31000,7 @@ class RemotePrivatePublicationsClient {
31000
31000
  return bad();
31001
31001
  const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
31002
31002
  const p = record7(response) && response.privatePublishing;
31003
- if (!record7(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || p.executionEnabled !== false)
31003
+ if (!record7(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || typeof p.executionEnabled !== "boolean")
31004
31004
  throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
31005
31005
  return Object.freeze({ ...p });
31006
31006
  }
@@ -32271,7 +32271,7 @@ async function preparePrivatePublication(client, sourceDirectory, recoveryDirect
32271
32271
  }
32272
32272
  function privatePublicationResult(directory, receipt) {
32273
32273
  const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
32274
- const nextAction = committed ? "The version is published. Private execution remains unavailable." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
32274
+ const nextAction = committed ? "Published; execution requires a separate server quote and approval." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
32275
32275
  return {
32276
32276
  recoveryDirectory: directory,
32277
32277
  skillId: receipt.skillId,
@@ -32279,7 +32279,7 @@ function privatePublicationResult(directory, receipt) {
32279
32279
  state,
32280
32280
  versionId: receipt.intent?.versionId ?? null,
32281
32281
  committed,
32282
- executionEnabled: false,
32282
+ executionEnabled: null,
32283
32283
  nextAction
32284
32284
  };
32285
32285
  }
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join6 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.5.6",
10
+ version: "0.5.8",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -23100,7 +23100,7 @@ var init_dist_es9 = __esm(() => {
23100
23100
  // package.json
23101
23101
  var package_default = {
23102
23102
  name: "@hasna/skills",
23103
- version: "0.5.6",
23103
+ version: "0.5.8",
23104
23104
  description: "Skills library for AI coding agents",
23105
23105
  type: "module",
23106
23106
  bin: {
package/bin/worker.js CHANGED
@@ -23103,7 +23103,7 @@ import { randomUUID as randomUUID4 } from "crypto";
23103
23103
  // package.json
23104
23104
  var package_default = {
23105
23105
  name: "@hasna/skills",
23106
- version: "0.5.6",
23106
+ version: "0.5.8",
23107
23107
  description: "Skills library for AI coding agents",
23108
23108
  type: "module",
23109
23109
  bin: {
package/dist/index.js CHANGED
@@ -13336,7 +13336,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
13336
13336
  // package.json
13337
13337
  var package_default = {
13338
13338
  name: "@hasna/skills",
13339
- version: "0.5.6",
13339
+ version: "0.5.8",
13340
13340
  description: "Skills library for AI coding agents",
13341
13341
  type: "module",
13342
13342
  bin: {
@@ -14412,7 +14412,7 @@ var privatePublicationContracts = [
14412
14412
  ].map((operation) => ({
14413
14413
  name: operation.name,
14414
14414
  title: operation.title,
14415
- description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; private execution remains unavailable.",
14415
+ description: "Manage private source publication with fresh workspace verification and durable host-local recovery. Upload consent and current version comparison are explicit; execution requires a separate server quote and approval.",
14416
14416
  params: [...Object.keys(publicationVerification), ...Object.keys(operation.extras)],
14417
14417
  category: "storage",
14418
14418
  sideEffects: "filesystem",
@@ -14425,7 +14425,7 @@ var privatePublicationContracts = [
14425
14425
  state: { type: "string" },
14426
14426
  versionId: { oneOf: [publicationUuidSchema, { type: "null" }] },
14427
14427
  committed: { type: "boolean" },
14428
- executionEnabled: { const: false },
14428
+ executionEnabled: { oneOf: [{ type: "boolean" }, { type: "null" }] },
14429
14429
  nextAction: { type: "string" }
14430
14430
  }, ["recoveryDirectory", "skillId", "intentId", "state", "versionId", "committed", "executionEnabled", "nextAction"])
14431
14431
  }));
@@ -16119,7 +16119,7 @@ class RemotePrivatePublicationsClient {
16119
16119
  return bad();
16120
16120
  const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
16121
16121
  const p = record5(response) && response.privatePublishing;
16122
- if (!record5(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || p.executionEnabled !== false)
16122
+ if (!record5(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p.contractVersion !== 1 || typeof p.enabled !== "boolean" || p.authentication !== "interactive-session" || p.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p.uploadMaxTtlSeconds !== 300 || typeof p.executionEnabled !== "boolean")
16123
16123
  throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
16124
16124
  return Object.freeze({ ...p });
16125
16125
  }
@@ -16642,7 +16642,7 @@ async function preparePrivatePublication(client, sourceDirectory, recoveryDirect
16642
16642
  }
16643
16643
  function privatePublicationResult(directory, receipt) {
16644
16644
  const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
16645
- const nextAction = committed ? "The version is published. Private execution remains unavailable." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
16645
+ const nextAction = committed ? "Published; execution requires a separate server quote and approval." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
16646
16646
  return {
16647
16647
  recoveryDirectory: directory,
16648
16648
  skillId: receipt.skillId,
@@ -16650,7 +16650,7 @@ function privatePublicationResult(directory, receipt) {
16650
16650
  state,
16651
16651
  versionId: receipt.intent?.versionId ?? null,
16652
16652
  committed,
16653
- executionEnabled: false,
16653
+ executionEnabled: null,
16654
16654
  nextAction
16655
16655
  };
16656
16656
  }
@@ -18,7 +18,8 @@ export interface PrivatePublicationResult {
18
18
  state: string;
19
19
  versionId: string | null;
20
20
  committed: boolean;
21
- executionEnabled: false;
21
+ /** Null when this recovery receipt contains no directly observed server capability. Publication alone never proves execution availability. */
22
+ executionEnabled: boolean | null;
22
23
  nextAction: string;
23
24
  }
24
25
  /** Reads only two bounded local files. The receipt never contains a token or signed URL. */
@@ -27,7 +27,8 @@ export interface PrivatePublishingCapability {
27
27
  authentication: "interactive-session";
28
28
  maxArchiveBytes: 16777216;
29
29
  uploadMaxTtlSeconds: 300;
30
- executionEnabled: false;
30
+ /** Server capability only; each execution still requires a separate quote and approval. */
31
+ executionEnabled: boolean;
31
32
  }
32
33
  /** Contains a short-lived bearer capability. Never print or persist this object. */
33
34
  export interface PrivatePublicationUpload {
@@ -48,3 +48,4 @@ export { WorkspaceInvitationInputError, RemoteWorkspaceInvitationError, RemoteWo
48
48
  export { InvitationEmailInputError, RemoteInvitationEmailError, RemoteInvitationEmailUnconfirmedError, type RequestInvitationEmailChallenge, type AcceptInvitationEmailChallenge, type RemoteInvitationEmailChallenge, type RemoteInvitationEmailAcceptance, type RemoteInvitationEmailErrorCode } from "../lib/remote-invitation-recovery.js";
49
49
  export { RemotePrivatePublicationsClient, PrivatePublicationError, PRIVATE_PUBLICATION_MAX_BYTES, type PrivatePublicationDeclaration, type PrivatePublicationView, type PrivatePublicationState, type PrivatePublishingCapability } from "../lib/remote-private-publications.js";
50
50
  export { preparePrivatePublication, readPrivatePublicationRecovery, continuePrivatePublication, inspectPrivatePublication, type PrivatePublicationRecovery, type PrivatePublicationResult } from "../lib/private-publication-recovery.js";
51
+ export { createSkillOperationClient, SkillOperationClientError, SKILL_OPERATION_LIMITS, type SkillOperationClient, type SkillOperationTransport, type SkillOperationRequest, type SkillOperationResult, type SkillOperationJson, type SkillOperationRefusal, type SkillOperationClientErrorCode } from "./operations.js";
package/dist/sdk/index.js CHANGED
@@ -25496,7 +25496,7 @@ class MissingSkillsFleetError extends Error {
25496
25496
  // package.json
25497
25497
  var package_default = {
25498
25498
  name: "@hasna/skills",
25499
- version: "0.5.6",
25499
+ version: "0.5.8",
25500
25500
  description: "Skills library for AI coding agents",
25501
25501
  type: "module",
25502
25502
  bin: {
@@ -48699,7 +48699,7 @@ var DEFAULT_IMAGE_PROFILES = {
48699
48699
  function createImageProfileRegistry(config = DEFAULT_IMAGE_PROFILES) {
48700
48700
  const runtimes = new Map;
48701
48701
  for (const pinned of config.runtimes) {
48702
- runtimes.set(pinned.runtime, pinned);
48702
+ runtimes.set(pinned.runtime, { ...pinned });
48703
48703
  }
48704
48704
  const allowlist = new Map(Object.entries(config.dependencyLayers));
48705
48705
  return {
@@ -48714,11 +48714,14 @@ function createImageProfileRegistry(config = DEFAULT_IMAGE_PROFILES) {
48714
48714
  if (systemDeps.length > 0 && dependencyLayerTag === null) {
48715
48715
  throw new ImageProfileResolutionError({ reason: "UNALLOWED_SYSTEM_DEPS", systemDeps });
48716
48716
  }
48717
- return { runtime: pinned, runtimeImageDigest: pinned.imageDigest, dependencyLayerTag };
48717
+ return { runtime: { ...pinned }, runtimeImageDigest: pinned.imageDigest, dependencyLayerTag };
48718
48718
  }
48719
48719
  };
48720
48720
  }
48721
48721
  function canonicalSystemDepsKey(systemDeps) {
48722
+ if (systemDeps.some((dep) => typeof dep !== "string" || dep.length === 0 || dep.includes(","))) {
48723
+ throw new ImageProfileResolutionError({ reason: "UNALLOWED_SYSTEM_DEPS", systemDeps: [...systemDeps] });
48724
+ }
48722
48725
  return Array.from(new Set(systemDeps)).sort().join(",");
48723
48726
  }
48724
48727
  function dependencyLayerRule(layerTag, systemDeps) {
@@ -48763,10 +48766,17 @@ function createSubmitRunService(options) {
48763
48766
  async submit(input) {
48764
48767
  if (!input.idempotencyKey.trim())
48765
48768
  throw new Error("admission: idempotencyKey is required");
48769
+ const policy = { ...DEFAULT_RUN_POLICY, ...input.policy };
48770
+ input = {
48771
+ ...input,
48772
+ systemDeps: [...input.systemDeps ?? []],
48773
+ policy: { ...policy, egressAllowlist: [...policy.egressAllowlist] },
48774
+ limits: { ...DEFAULT_RUN_LIMITS, ...input.limits }
48775
+ };
48776
+ const inputDigest = digestInput(input.input);
48766
48777
  const byKey = await store.getRunByKey(input.tenantId, input.idempotencyKey);
48767
48778
  if (byKey)
48768
- return { run: byKey.admission, created: false };
48769
- const inputDigest = digestInput(input.input);
48779
+ return { run: ownedAdmission(byKey.admission), created: false };
48770
48780
  const byDigests = await store.getRunByDigests({
48771
48781
  tenantId: input.tenantId,
48772
48782
  skillId: input.skillId,
@@ -48775,7 +48785,7 @@ function createSubmitRunService(options) {
48775
48785
  inputDigest
48776
48786
  });
48777
48787
  if (byDigests)
48778
- return { run: byDigests.admission, created: false };
48788
+ return { run: ownedAdmission(byDigests.admission), created: false };
48779
48789
  const image = resolveImageProfile(imageProfiles, {
48780
48790
  runtime: input.runtime,
48781
48791
  systemDeps: input.systemDeps ?? []
@@ -48797,10 +48807,17 @@ function createSubmitRunService(options) {
48797
48807
  createdAt: now().toISOString()
48798
48808
  };
48799
48809
  const row = await store.admit(admission);
48800
- return { run: row.admission, created: true };
48810
+ return { run: ownedAdmission(row.admission), created: true };
48801
48811
  }
48802
48812
  };
48803
48813
  }
48814
+ function ownedAdmission(admission) {
48815
+ return {
48816
+ ...admission,
48817
+ policy: { ...admission.policy, egressAllowlist: [...admission.policy.egressAllowlist] },
48818
+ limits: { ...admission.limits }
48819
+ };
48820
+ }
48804
48821
  function digestInput(input) {
48805
48822
  return createHash10("sha256").update(canonicalJson(input)).digest("hex");
48806
48823
  }
@@ -54447,7 +54464,7 @@ class RemotePrivatePublicationsClient {
54447
54464
  return bad();
54448
54465
  const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
54449
54466
  const p2 = record5(response) && response.privatePublishing;
54450
- if (!record5(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p2, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p2.contractVersion !== 1 || typeof p2.enabled !== "boolean" || p2.authentication !== "interactive-session" || p2.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p2.uploadMaxTtlSeconds !== 300 || p2.executionEnabled !== false)
54467
+ if (!record5(response) || response.contractVersion !== 1 || response.apiVersion !== 1 || !exact(p2, ["contractVersion", "enabled", "authentication", "maxArchiveBytes", "uploadMaxTtlSeconds", "executionEnabled"]) || p2.contractVersion !== 1 || typeof p2.enabled !== "boolean" || p2.authentication !== "interactive-session" || p2.maxArchiveBytes !== PRIVATE_PUBLICATION_MAX_BYTES || p2.uploadMaxTtlSeconds !== 300 || typeof p2.executionEnabled !== "boolean")
54451
54468
  throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
54452
54469
  return Object.freeze({ ...p2 });
54453
54470
  }
@@ -54970,7 +54987,7 @@ async function preparePrivatePublication(client, sourceDirectory, recoveryDirect
54970
54987
  }
54971
54988
  function privatePublicationResult(directory, receipt) {
54972
54989
  const state = receipt.intent?.state ?? receipt.phase, committed = state === "committed";
54973
- const nextAction = committed ? "The version is published. Private execution remains unavailable." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
54990
+ const nextAction = committed ? "Published; execution requires a separate server quote and approval." : ["rejected", "cancelled", "expired"].includes(state) ? "This intent is terminal. Inspect the result before explicitly preparing another version." : state === "needs_attention" ? "Keep this intent and contact the service operator; do not create a replacement or upload again." : receipt.phase === "upload_uncertain" ? "Run publication resume with this recovery directory to finalize the same intent without another upload." : receipt.intent ? "Run publication status or resume with this recovery directory; cancel explicitly if you want to stop." : "Run publication resume with this recovery directory to reconcile the identical request key and declaration.";
54974
54991
  return {
54975
54992
  recoveryDirectory: directory,
54976
54993
  skillId: receipt.skillId,
@@ -54978,7 +54995,7 @@ function privatePublicationResult(directory, receipt) {
54978
54995
  state,
54979
54996
  versionId: receipt.intent?.versionId ?? null,
54980
54997
  committed,
54981
- executionEnabled: false,
54998
+ executionEnabled: null,
54982
54999
  nextAction
54983
55000
  };
54984
55001
  }
@@ -55044,6 +55061,206 @@ async function inspectLocked(client, directory, cancel) {
55044
55061
  throw new PrivatePublicationError("PUBLICATION_INTENT_UNKNOWN", "Reconcile the saved begin request with publication resume before cancelling its intent.");
55045
55062
  return privatePublicationResult(directory, receipt);
55046
55063
  }
55064
+ // src/sdk/operations.ts
55065
+ var SKILL_OPERATION_LIMITS = Object.freeze({ requestBytes: 65536, resultBytes: 1048576, depth: 32, nodes: 16384, rememberedRequests: 256, rememberedBytes: 1048576 });
55066
+
55067
+ class SkillOperationClientError extends Error {
55068
+ code;
55069
+ outcome;
55070
+ constructor(code, outcome) {
55071
+ super(`Skill operation client: ${code}`);
55072
+ this.code = code;
55073
+ this.outcome = outcome;
55074
+ this.name = "SkillOperationClientError";
55075
+ }
55076
+ }
55077
+ var encoder = new TextEncoder;
55078
+ var refusalCodes = new Set(["NOT_ALLOWED", "APPROVAL_REQUIRED", "BUDGET_EXHAUSTED", "EXPIRED", "CANCELLED", "UNAVAILABLE", "INVALID_INPUT"]);
55079
+ var requestIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
55080
+ function validId(value) {
55081
+ return typeof value === "string" && requestIdPattern.test(value);
55082
+ }
55083
+ function record6(value) {
55084
+ return !!value && typeof value === "object" && !Array.isArray(value);
55085
+ }
55086
+ function exact2(value, names) {
55087
+ const keys = Object.keys(value).sort(), expected = [...names].sort();
55088
+ return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
55089
+ }
55090
+ function snapshot(value, maximum) {
55091
+ let nodes4 = 0, bytes = 0;
55092
+ const parts = [], ancestors = new Set;
55093
+ const append = (part) => {
55094
+ if (part.length > maximum - bytes)
55095
+ throw Error();
55096
+ bytes += encoder.encode(part).byteLength;
55097
+ if (bytes > maximum)
55098
+ throw Error();
55099
+ parts.push(part);
55100
+ };
55101
+ function visit(input, depth) {
55102
+ if (++nodes4 > SKILL_OPERATION_LIMITS.nodes || depth > SKILL_OPERATION_LIMITS.depth)
55103
+ throw Error();
55104
+ if (input === null || typeof input === "boolean") {
55105
+ append(String(input));
55106
+ return input;
55107
+ }
55108
+ if (typeof input === "number") {
55109
+ if (!Number.isFinite(input))
55110
+ throw Error();
55111
+ append(JSON.stringify(input));
55112
+ return Object.is(input, -0) ? 0 : input;
55113
+ }
55114
+ if (typeof input === "string") {
55115
+ if (input.length > maximum - bytes || /[\ud800-\udfff]/u.test(input))
55116
+ throw Error();
55117
+ append(JSON.stringify(input));
55118
+ return input;
55119
+ }
55120
+ if (!input || typeof input !== "object" || ancestors.has(input))
55121
+ throw Error();
55122
+ const array = Array.isArray(input), proto = Object.getPrototypeOf(input);
55123
+ if (array ? proto !== Array.prototype : proto !== Object.prototype && proto !== null)
55124
+ throw Error();
55125
+ const keys = Reflect.ownKeys(input);
55126
+ if (keys.length > SKILL_OPERATION_LIMITS.nodes + 1 || keys.some((key) => typeof key !== "string"))
55127
+ throw Error();
55128
+ ancestors.add(input);
55129
+ try {
55130
+ if (array) {
55131
+ if (input.length > SKILL_OPERATION_LIMITS.nodes || keys.length !== input.length + 1)
55132
+ throw Error();
55133
+ const result2 = [];
55134
+ append("[");
55135
+ for (let i4 = 0;i4 < input.length; i4++) {
55136
+ const descriptor = Object.getOwnPropertyDescriptor(input, String(i4));
55137
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable)
55138
+ throw Error();
55139
+ if (i4)
55140
+ append(",");
55141
+ result2.push(visit(descriptor.value, depth + 1));
55142
+ }
55143
+ append("]");
55144
+ return Object.freeze(result2);
55145
+ }
55146
+ const result = Object.create(null);
55147
+ append("{");
55148
+ for (const [i4, key] of keys.sort().entries()) {
55149
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
55150
+ if (!("value" in descriptor) || !descriptor.enumerable || /[\ud800-\udfff]/u.test(key) || key.length > maximum - bytes)
55151
+ throw Error();
55152
+ if (i4)
55153
+ append(",");
55154
+ append(JSON.stringify(key));
55155
+ append(":");
55156
+ result[key] = visit(descriptor.value, depth + 1);
55157
+ }
55158
+ append("}");
55159
+ return Object.freeze(result);
55160
+ } finally {
55161
+ ancestors.delete(input);
55162
+ }
55163
+ }
55164
+ const owned = visit(value, 0);
55165
+ return { value: owned, canonical: parts.join(""), bytes };
55166
+ }
55167
+ function requestSnapshot(input) {
55168
+ try {
55169
+ const owned = snapshot(input, SKILL_OPERATION_LIMITS.requestBytes), value = owned.value;
55170
+ if (!record6(value) || !exact2(value, ["contractVersion", "requestId", "operation", "input"]) || value.contractVersion !== 1 || !validId(value.requestId) || typeof value.operation !== "string" || value.operation.length > 128 || !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(value.operation) || !record6(value.input))
55171
+ throw Error();
55172
+ return { ...owned, value };
55173
+ } catch {
55174
+ throw new SkillOperationClientError("INVALID_REQUEST", "not-invoked");
55175
+ }
55176
+ }
55177
+ function resultSnapshot(input, requestId) {
55178
+ try {
55179
+ const value = snapshot(input, SKILL_OPERATION_LIMITS.resultBytes).value;
55180
+ if (!record6(value) || value.contractVersion !== 1 || value.requestId !== requestId)
55181
+ throw Error();
55182
+ const fields = ["contractVersion", "requestId", "status"];
55183
+ if (value.status === "succeeded")
55184
+ fields.push("output");
55185
+ else if (value.status === "refused") {
55186
+ fields.push("code");
55187
+ if (!refusalCodes.has(value.code))
55188
+ throw Error();
55189
+ } else if (!["not-executed", "pending", "unknown"].includes(value.status))
55190
+ throw Error();
55191
+ if (!exact2(value, fields))
55192
+ throw Error();
55193
+ return value;
55194
+ } catch {
55195
+ throw new SkillOperationClientError("INVALID_RESPONSE", "unknown");
55196
+ }
55197
+ }
55198
+ function createSkillOperationClient(transport, options = {}) {
55199
+ const timeoutMs = options.timeoutMs ?? 30000;
55200
+ if (!transport || typeof transport.invoke !== "function" || typeof transport.get !== "function" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60000)
55201
+ throw new SkillOperationClientError("INVALID_CONFIGURATION", "not-invoked");
55202
+ const invokeTransport = transport.invoke.bind(transport), getTransport = transport.get.bind(transport);
55203
+ const identities = new Map;
55204
+ let rememberedBytes = 0;
55205
+ async function call(requestId, signal, invoke) {
55206
+ if (signal?.aborted)
55207
+ throw new SkillOperationClientError("ABORTED", "not-invoked");
55208
+ const controller = new AbortController;
55209
+ let timer;
55210
+ const abort = () => controller.abort();
55211
+ let onAbort = () => {};
55212
+ const interrupted = new Promise((_, reject) => {
55213
+ onAbort = () => reject(new SkillOperationClientError("UNKNOWN_OUTCOME", "unknown"));
55214
+ controller.signal.addEventListener("abort", onAbort, { once: true });
55215
+ });
55216
+ signal?.addEventListener("abort", abort, { once: true });
55217
+ try {
55218
+ if (signal?.aborted)
55219
+ throw new SkillOperationClientError("ABORTED", "not-invoked");
55220
+ timer = setTimeout(abort, timeoutMs);
55221
+ let pending;
55222
+ try {
55223
+ pending = invoke(controller.signal);
55224
+ } catch {
55225
+ pending = Promise.reject(new SkillOperationClientError("UNKNOWN_OUTCOME", "unknown"));
55226
+ }
55227
+ const owned = Promise.resolve(pending).then((value) => resultSnapshot(value, requestId), () => {
55228
+ throw new SkillOperationClientError("UNKNOWN_OUTCOME", "unknown");
55229
+ });
55230
+ const result = await Promise.race([owned, interrupted]);
55231
+ if (controller.signal.aborted)
55232
+ throw new SkillOperationClientError("UNKNOWN_OUTCOME", "unknown");
55233
+ return result;
55234
+ } finally {
55235
+ if (timer !== undefined)
55236
+ clearTimeout(timer);
55237
+ signal?.removeEventListener("abort", abort);
55238
+ controller.signal.removeEventListener("abort", onAbort);
55239
+ controller.abort();
55240
+ }
55241
+ }
55242
+ return Object.freeze({
55243
+ async invoke(input, requestOptions = {}) {
55244
+ if (requestOptions.signal?.aborted)
55245
+ throw new SkillOperationClientError("ABORTED", "not-invoked");
55246
+ const owned = requestSnapshot(input), previous = identities.get(owned.value.requestId);
55247
+ if (previous !== undefined && previous !== owned.canonical)
55248
+ throw new SkillOperationClientError("REQUEST_CONFLICT", "not-invoked");
55249
+ if (previous === undefined) {
55250
+ if (identities.size >= SKILL_OPERATION_LIMITS.rememberedRequests || rememberedBytes + owned.bytes > SKILL_OPERATION_LIMITS.rememberedBytes)
55251
+ throw new SkillOperationClientError("REQUEST_CAPACITY", "not-invoked");
55252
+ identities.set(owned.value.requestId, owned.canonical);
55253
+ rememberedBytes += owned.bytes;
55254
+ }
55255
+ return call(owned.value.requestId, requestOptions.signal, (signal) => invokeTransport(owned.value, { signal }));
55256
+ },
55257
+ async get(requestId, requestOptions = {}) {
55258
+ if (!validId(requestId))
55259
+ throw new SkillOperationClientError("INVALID_REQUEST", "not-invoked");
55260
+ return call(requestId, requestOptions.signal, (signal) => getTransport(requestId, { signal }));
55261
+ }
55262
+ });
55263
+ }
55047
55264
  export {
55048
55265
  verifyContentHashFromEntries,
55049
55266
  validateRunLifecycleEvent,
@@ -55102,6 +55319,7 @@ export {
55102
55319
  createStore,
55103
55320
  createSpendService,
55104
55321
  createSkillsFetchHandler,
55322
+ createSkillOperationClient,
55105
55323
  createServer,
55106
55324
  createRunStateMachine,
55107
55325
  createRunService,
@@ -55134,7 +55352,9 @@ export {
55134
55352
  SqliteRunExecutionStore,
55135
55353
  SqliteGovernanceStore,
55136
55354
  SkillsFleetCredentialError,
55355
+ SkillOperationClientError,
55137
55356
  SkillBundleInspectionError,
55357
+ SKILL_OPERATION_LIMITS,
55138
55358
  SKILL_BUNDLE_INSPECTION_LIMITS,
55139
55359
  SKILLS_LOCAL_OPT_IN_ENV_KEYS,
55140
55360
  SKILLS_APP,
@@ -0,0 +1,68 @@
1
+ /** Provider-neutral guest operation client. Authority and IPC belong to the embedder.
2
+ * No credentials, endpoint discovery, provider SDK, or automatic retry is provided.
3
+ */
4
+ export type SkillOperationJson = null | boolean | number | string | readonly SkillOperationJson[] | {
5
+ readonly [key: string]: SkillOperationJson;
6
+ };
7
+ export declare const SKILL_OPERATION_LIMITS: Readonly<{
8
+ requestBytes: 65536;
9
+ resultBytes: 1048576;
10
+ depth: 32;
11
+ nodes: 16384;
12
+ rememberedRequests: 256;
13
+ rememberedBytes: 1048576;
14
+ }>;
15
+ export type SkillOperationRefusal = "NOT_ALLOWED" | "APPROVAL_REQUIRED" | "BUDGET_EXHAUSTED" | "EXPIRED" | "CANCELLED" | "UNAVAILABLE" | "INVALID_INPUT";
16
+ export interface SkillOperationRequest {
17
+ readonly contractVersion: 1;
18
+ readonly requestId: string;
19
+ readonly operation: string;
20
+ readonly input: {
21
+ readonly [key: string]: SkillOperationJson;
22
+ };
23
+ }
24
+ interface OperationIdentity {
25
+ readonly contractVersion: 1;
26
+ readonly requestId: string;
27
+ }
28
+ export type SkillOperationResult = OperationIdentity & ({
29
+ readonly status: "not-executed" | "pending" | "unknown";
30
+ } | {
31
+ readonly status: "succeeded";
32
+ readonly output: SkillOperationJson;
33
+ } | {
34
+ readonly status: "refused";
35
+ readonly code: SkillOperationRefusal;
36
+ });
37
+ /** Implementations must authenticate the captured run/attempt separately. A
38
+ * not-executed response is authoritative proof, never a guess from transport loss. */
39
+ export interface SkillOperationTransport {
40
+ invoke(request: SkillOperationRequest, options: {
41
+ signal: AbortSignal;
42
+ }): Promise<unknown>;
43
+ get(requestId: string, options: {
44
+ signal: AbortSignal;
45
+ }): Promise<unknown>;
46
+ }
47
+ export type SkillOperationClientErrorCode = "INVALID_REQUEST" | "REQUEST_CONFLICT" | "REQUEST_CAPACITY" | "ABORTED" | "UNKNOWN_OUTCOME" | "INVALID_RESPONSE" | "INVALID_CONFIGURATION";
48
+ export declare class SkillOperationClientError extends Error {
49
+ readonly code: SkillOperationClientErrorCode;
50
+ readonly outcome: "not-invoked" | "unknown";
51
+ constructor(code: SkillOperationClientErrorCode, outcome: "not-invoked" | "unknown");
52
+ }
53
+ export interface SkillOperationClient {
54
+ invoke(request: SkillOperationRequest, options?: {
55
+ signal?: AbortSignal;
56
+ }): Promise<SkillOperationResult>;
57
+ get(requestId: string, options?: {
58
+ signal?: AbortSignal;
59
+ }): Promise<SkillOperationResult>;
60
+ }
61
+ /** One client belongs to one captured authority scope. Remembered identities
62
+ * are bounded and never evicted. This is local misuse protection, not durable
63
+ * deduplication: the server must bind request IDs and payloads atomically.
64
+ * Each explicit call invokes transport once; uncertainty requires explicit get. */
65
+ export declare function createSkillOperationClient(transport: SkillOperationTransport, options?: {
66
+ timeoutMs?: number;
67
+ }): SkillOperationClient;
68
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {