@hasna/skills 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sdk/index.js CHANGED
@@ -23115,6 +23115,7 @@ import { spawn } from "child_process";
23115
23115
  import { request as nodeHttpRequest } from "http";
23116
23116
  import { request as nodeHttpsRequest } from "https";
23117
23117
  import { randomUUID as randomUUID22 } from "crypto";
23118
+ import { createRequire as createRequire2 } from "module";
23118
23119
  function getPathValue(input, path) {
23119
23120
  return path.split(".").reduce((value, part) => {
23120
23121
  if (value && typeof value === "object" && part in value) {
@@ -24330,7 +24331,7 @@ function normalizeRetryPolicy(policy) {
24330
24331
  }
24331
24332
  var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE2, HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", EVENTS_STORE_SENTINEL_FILE = "events.json", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, DEFAULT_MAX_REDIRECTS = 5, IPV4_PRIVATE_RANGES, IPV6_SPECIAL_PREFIXES, defaultTargetLookup = async (hostname) => {
24332
24333
  return dnsLookup(hostname, { all: true, verbatim: false });
24333
- }, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
24334
+ }, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES, MAX_CREDENTIAL_FILE_BYTES2, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2, MAX_ENVELOPE_BYTES, MAX_REQUEST_BYTES;
24334
24335
  var init_dist = __esm(() => {
24335
24336
  PATHS_RESOLVER_KIND_ENV = {
24336
24337
  config: "HASNA_CONFIG_HOME",
@@ -24386,6 +24387,22 @@ var init_dist = __esm(() => {
24386
24387
  };
24387
24388
  defaultEventTypeCatalog = new EventTypeCatalog;
24388
24389
  APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
24390
+ MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
24391
+ INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
24392
+ CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
24393
+ AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
24394
+ SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
24395
+ requireSecretsSdk2 = createRequire2(import.meta.url);
24396
+ IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
24397
+ AUTHORITY_OVERRIDE_HEADERS2 = new Set([
24398
+ "host",
24399
+ ":authority",
24400
+ "forwarded",
24401
+ "x-forwarded-host",
24402
+ "x-original-host"
24403
+ ]);
24404
+ MAX_ENVELOPE_BYTES = 256 * 1024;
24405
+ MAX_REQUEST_BYTES = MAX_ENVELOPE_BYTES * 2 + 8192;
24389
24406
  });
24390
24407
 
24391
24408
  // src/sdk/event-sink.ts
@@ -25237,7 +25254,9 @@ function normalizeSkillsApiOrigin(apiUrl) {
25237
25254
  throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
25238
25255
  }
25239
25256
  const pathname = url.pathname.replace(/\/+$/, "");
25240
- if (pathname === "/api" || pathname === "/api/v1") {
25257
+ if (url.origin === "https://api.hasna.com" && pathname === "/skills/v1") {
25258
+ url.pathname = "/skills";
25259
+ } else if (pathname === "/api" || pathname === "/api/v1") {
25241
25260
  url.pathname = "/";
25242
25261
  } else if (pathname.endsWith("/api/v1")) {
25243
25262
  url.pathname = pathname.slice(0, -"/api/v1".length) || "/";
@@ -25246,6 +25265,21 @@ function normalizeSkillsApiOrigin(apiUrl) {
25246
25265
  }
25247
25266
  return url.toString().replace(/\/+$/, "");
25248
25267
  }
25268
+ function skillsApiRequestUrl(apiUrl, route) {
25269
+ const origin = normalizeSkillsApiOrigin(apiUrl);
25270
+ if (!route.startsWith("/api/") || route.includes("#") || route.includes("\\")) {
25271
+ throw new SkillsFleetCredentialError("Invalid Skills API route", "INVALID_API_URL");
25272
+ }
25273
+ if (origin === "https://api.hasna.com/skills") {
25274
+ if (route === "/api/auth/whoami")
25275
+ return `${origin}/v1/auth/whoami`;
25276
+ if (!route.startsWith("/api/v1/")) {
25277
+ throw new SkillsFleetCredentialError("The internal Skills gateway has no established login contract yet. Select an explicitly configured instance with supported authentication.", "GATEWAY_AUTH_UNAVAILABLE");
25278
+ }
25279
+ return `${origin}${route.slice("/api".length)}`;
25280
+ }
25281
+ return `${origin}${route}`;
25282
+ }
25249
25283
  function configuredSkillsApiUrl(env = process.env, keychain, profile) {
25250
25284
  const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
25251
25285
  for (const entry of declared) {
@@ -25462,7 +25496,7 @@ class MissingSkillsFleetError extends Error {
25462
25496
  // package.json
25463
25497
  var package_default = {
25464
25498
  name: "@hasna/skills",
25465
- version: "0.5.3",
25499
+ version: "0.5.5",
25466
25500
  description: "Skills library for AI coding agents",
25467
25501
  type: "module",
25468
25502
  bin: {
@@ -37097,10 +37131,16 @@ class SqliteSkillsStore {
37097
37131
  const orgId = input.principal.orgId;
37098
37132
  const now = nowIso();
37099
37133
  return this.db.transaction(() => {
37100
- const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
37134
+ const previous = this.get("SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at, source FROM skills_registry WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
37101
37135
  const previousSha = typeof previous?.bundle_sha256 === "string" ? previous.bundle_sha256 : null;
37102
37136
  const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? previous.revision_id : null;
37103
37137
  const tombstoned = previous?.tombstoned_at != null;
37138
+ if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
37139
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
37140
+ }
37141
+ if (input.seedBundledOnly && previous && (tombstoned || previous.source !== "bundled")) {
37142
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
37143
+ }
37104
37144
  const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? previous.skill_md : null;
37105
37145
  if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
37106
37146
  throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
@@ -37174,8 +37214,9 @@ class SqliteSkillsStore {
37174
37214
  tombstoned_at = NULL,
37175
37215
  tombstone_purge_after = NULL,
37176
37216
  updated_at = excluded.updated_at
37177
- WHERE skills_registry.tombstoned_at IS NOT NULL
37178
- OR skills_registry.revision_id = ?
37217
+ WHERE (skills_registry.tombstoned_at IS NOT NULL OR skills_registry.revision_id = ?)
37218
+ AND (? = 0 OR (skills_registry.tombstoned_at IS NULL AND skills_registry.source = 'bundled'
37219
+ AND skills_registry.revision_id = ?))
37179
37220
  RETURNING *`, [
37180
37221
  orgId,
37181
37222
  input.slug,
@@ -37193,6 +37234,8 @@ class SqliteSkillsStore {
37193
37234
  revisionId,
37194
37235
  now,
37195
37236
  now,
37237
+ input.expectedRevisionId ?? NO_REVISION_SENTINEL,
37238
+ input.seedBundledOnly ? 1 : 0,
37196
37239
  input.expectedRevisionId ?? NO_REVISION_SENTINEL
37197
37240
  ]);
37198
37241
  if (!row) {
@@ -42238,6 +42281,12 @@ async function storePublishedSkill(store, artifactStorage, principal, parsed, ex
42238
42281
  principal,
42239
42282
  ...expectedRevisionId ? { expectedRevisionId } : {}
42240
42283
  };
42284
+ if (input.seedBundledOnly && expectedRevisionId && !current) {
42285
+ throw new SkillRevisionConflictError(input.slug, expectedRevisionId, null);
42286
+ }
42287
+ if (input.seedBundledOnly && current && (current.tombstonedAt || current.source !== "bundled")) {
42288
+ throw new SkillRevisionConflictError(input.slug, expectedRevisionId, current.revisionId);
42289
+ }
42241
42290
  if (current && !current.tombstonedAt && expectedRevisionId !== current.revisionId) {
42242
42291
  throw new SkillRevisionConflictError(input.slug, expectedRevisionId, current.revisionId);
42243
42292
  }
@@ -42487,6 +42536,15 @@ async function seedBundledCorpus(options) {
42487
42536
  result.skipped.push(slug);
42488
42537
  continue;
42489
42538
  }
42539
+ const current = await options.store.getSkill(options.principal, slug);
42540
+ if (current) {
42541
+ const previousVersion = current.version ? await options.store.getSkillVersion(options.principal, slug, current.version) : null;
42542
+ const provenance = previousVersion?.manifest.provenance;
42543
+ if (current.tombstonedAt || current.source !== "bundled" || provenance?.seededFrom !== "bundled-corpus" || provenance.seededRevisionId !== current.revisionId) {
42544
+ result.skipped.push(slug);
42545
+ continue;
42546
+ }
42547
+ }
42490
42548
  const dir = getSkillPath(slug);
42491
42549
  if (!existsSync15(dir)) {
42492
42550
  result.skipped.push(slug);
@@ -42496,32 +42554,40 @@ async function seedBundledCorpus(options) {
42496
42554
  const packed = packSkillBundle(dir, { maxUnpackedBytes: 50000000 });
42497
42555
  const skillMdPath = join19(dir, "SKILL.md");
42498
42556
  const skillMd = existsSync15(skillMdPath) ? readFileSync16(skillMdPath, "utf-8") : undefined;
42499
- const current = await options.store.getSkill(options.principal, slug);
42557
+ const content = {
42558
+ slug,
42559
+ displayName: manifest.displayName ?? skill.displayName ?? slug,
42560
+ description: manifest.description ?? skill.description ?? slug,
42561
+ category: manifest.category ?? skill.category ?? "Development Tools",
42562
+ tags: manifest.tags ?? skill.tags ?? [],
42563
+ source: "bundled",
42564
+ kind: manifest.kind ?? "instruction",
42565
+ version: version2,
42566
+ ...skillMd ? { skillMd } : {},
42567
+ bundleSha256: packed.sha256,
42568
+ bundleByteSize: packed.bytes.byteLength
42569
+ };
42500
42570
  const { record } = await storePublishedSkill(options.store, options.artifactStorage, options.principal, {
42501
42571
  input: {
42502
- slug,
42503
- displayName: manifest.displayName ?? skill.displayName ?? slug,
42504
- description: manifest.description ?? skill.description ?? slug,
42505
- category: manifest.category ?? skill.category ?? "Development Tools",
42506
- tags: manifest.tags ?? skill.tags ?? [],
42507
- source: "bundled",
42508
- kind: manifest.kind ?? "instruction",
42509
- version: version2,
42510
- ...skillMd ? { skillMd } : {},
42572
+ ...content,
42573
+ seedBundledOnly: true,
42511
42574
  bundle: { sha256: packed.sha256, byteSize: packed.bytes.byteLength, contentType: "application/gzip", storageKind: "db" },
42512
42575
  versionManifest: {
42513
42576
  files: packed.paths,
42514
42577
  fileCount: packed.fileCount,
42515
42578
  unpackedByteSize: packed.unpackedByteSize,
42516
42579
  bundleSha256: packed.sha256,
42517
- provenance: { seededFrom: "bundled-corpus", packageVersion: package_default.version }
42580
+ provenance: { seededFrom: "bundled-corpus", packageVersion: package_default.version, seededRevisionId: revisionIdOfRecord(content) }
42518
42581
  }
42519
42582
  },
42520
42583
  bundleBytes: packed.bytes
42521
42584
  }, current?.revisionId);
42522
42585
  result.seeded.push(`${record.slug}@${version2}`);
42523
42586
  } catch (error) {
42524
- result.failed.push({ slug, error: error.message });
42587
+ if (error instanceof SkillRevisionConflictError)
42588
+ result.skipped.push(slug);
42589
+ else
42590
+ result.failed.push({ slug, error: error.message });
42525
42591
  }
42526
42592
  }
42527
42593
  log(`skills: bundled corpus seed v${version2}: ${result.seeded.length} seeded, ${result.skipped.length} skipped, ${result.failed.length} failed`);
@@ -42731,6 +42797,12 @@ class MemorySkillsStore {
42731
42797
  const key = skillKey(input.principal.orgId, input.slug);
42732
42798
  const now = nowIso();
42733
42799
  const previous = this.skills.get(key);
42800
+ if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
42801
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
42802
+ }
42803
+ if (input.seedBundledOnly && previous && (previous.tombstonedAt || previous.source !== "bundled")) {
42804
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
42805
+ }
42734
42806
  if (previous && !previous.tombstonedAt && input.expectedRevisionId !== previous.revisionId) {
42735
42807
  throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previous.revisionId);
42736
42808
  }
@@ -43184,13 +43256,19 @@ class PostgresSkillsStore {
43184
43256
  const orgId = input.principal.orgId;
43185
43257
  return await this.sql.begin(async (tx) => {
43186
43258
  const previousRows = await tx`
43187
- SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at
43259
+ SELECT revision_id, revision_number, bundle_sha256, bundle_byte_size, skill_md, tombstoned_at, source
43188
43260
  FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} LIMIT 1
43189
43261
  `;
43190
43262
  const previous = previousRows[0];
43191
43263
  const previousSha = typeof previous?.bundle_sha256 === "string" ? String(previous.bundle_sha256) : null;
43192
43264
  const previousRevisionId = typeof previous?.revision_id === "string" && previous.revision_id ? String(previous.revision_id) : null;
43193
43265
  const tombstoned = previous?.tombstoned_at != null;
43266
+ if (input.seedBundledOnly && input.expectedRevisionId && !previous) {
43267
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
43268
+ }
43269
+ if (input.seedBundledOnly && previous && (tombstoned || previous.source !== "bundled")) {
43270
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
43271
+ }
43194
43272
  const carriedSkillMd = typeof input.skillMd === "string" ? input.skillMd : typeof previous?.skill_md === "string" ? String(previous.skill_md) : null;
43195
43273
  if (previous && !tombstoned && input.expectedRevisionId !== previousRevisionId) {
43196
43274
  throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
@@ -43231,6 +43309,12 @@ class PostgresSkillsStore {
43231
43309
  body_blob = EXCLUDED.body_blob
43232
43310
  `;
43233
43311
  }
43312
+ if (input.seedBundledOnly) {
43313
+ const seedRows = await tx`SELECT revision_id FROM skills_registry WHERE org_id = ${orgId} AND slug = ${input.slug} FOR UPDATE`;
43314
+ if (input.expectedRevisionId && !seedRows[0]) {
43315
+ throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, null);
43316
+ }
43317
+ }
43234
43318
  const rows = await tx`
43235
43319
  INSERT INTO skills_registry (org_id, slug, display_name, description, category, tags_json, source, kind, version, skill_md,
43236
43320
  bundle_sha256, bundle_byte_size, published_by_user_id, revision_id, revision_number, updated_at)
@@ -43259,8 +43343,11 @@ class PostgresSkillsStore {
43259
43343
  tombstoned_at = NULL,
43260
43344
  tombstone_purge_after = NULL,
43261
43345
  updated_at = EXCLUDED.updated_at
43262
- WHERE skills_registry.tombstoned_at IS NOT NULL
43263
- OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}
43346
+ WHERE (skills_registry.tombstoned_at IS NOT NULL
43347
+ OR skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2})
43348
+ AND (NOT ${Boolean(input.seedBundledOnly)} OR
43349
+ (skills_registry.tombstoned_at IS NULL AND skills_registry.source = 'bundled'
43350
+ AND skills_registry.revision_id = ${input.expectedRevisionId ?? NO_REVISION_SENTINEL2}))
43264
43351
  RETURNING *
43265
43352
  `;
43266
43353
  if (!rows[0]) {
@@ -43675,6 +43762,15 @@ async function createSkillsFetchHandler(options = {}) {
43675
43762
  }
43676
43763
  return async function fetch2(request) {
43677
43764
  const url = new URL(request.url);
43765
+ const versionedAliases = {
43766
+ "/v1/health": "/health",
43767
+ "/v1/auth/whoami": "/api/auth/whoami"
43768
+ };
43769
+ const alias = versionedAliases[url.pathname] ?? (url.pathname === "/v1" || url.pathname.startsWith("/v1/") ? `/api${url.pathname}` : undefined);
43770
+ if (alias) {
43771
+ url.pathname = alias;
43772
+ request = new Request(url, request);
43773
+ }
43678
43774
  const segments = pathSegments(url.pathname);
43679
43775
  try {
43680
43776
  if (request.method === "GET" && url.pathname === "/health") {
@@ -52098,7 +52194,13 @@ class StopTaskCommand extends command3(_ep03, _mw03, "StopTask", StopTask$) {
52098
52194
 
52099
52195
  // src/sdk/execution/dispatchers/ecs.ts
52100
52196
  var CLIENT_TOKEN_BYTES = 16;
52101
- var TERMINAL_TASK_STATUSES = new Set(["STOPPED"]);
52197
+ var LIVE_TASK_STATUSES = new Set(["PROVISIONING", "PENDING", "ACTIVATING", "RUNNING", "DEACTIVATING", "STOPPING", "DEPROVISIONING"]);
52198
+ function observedTask(states, taskArn) {
52199
+ if (states.length !== 1 || states[0]?.taskArn !== taskArn)
52200
+ return;
52201
+ const state = states[0];
52202
+ return state.lastStatus === "STOPPED" || LIVE_TASK_STATUSES.has(state.lastStatus) ? state : undefined;
52203
+ }
52102
52204
  function clientTokenFor(runId2, attemptId) {
52103
52205
  return createHash11("sha256").update(`${runId2}\x00${attemptId}`).digest("hex").slice(0, CLIENT_TOKEN_BYTES * 2);
52104
52206
  }
@@ -52150,28 +52252,47 @@ class EcsDispatcher {
52150
52252
  const run = await this.store.getRun(runId2);
52151
52253
  if (!run)
52152
52254
  return { accepted: false, detail: "no such run" };
52153
- if (run.status === "cancelled")
52154
- return { accepted: true, detail: "already cancelled" };
52155
52255
  if (run.status === "succeeded" || run.status === "failed") {
52156
52256
  return { accepted: false, detail: `run already ${run.status}` };
52157
52257
  }
52158
52258
  const attempts = await this.store.listAttempts(runId2);
52159
52259
  const current = attempts[attempts.length - 1];
52160
52260
  if (!current) {
52161
- const cancelled2 = await this.stateMachine.cancel(runId2);
52162
- return cancelled2.ok ? { accepted: true, detail: "cancelled (no attempt launched)" } : { accepted: false, detail: cancelled2.reason };
52261
+ if (run.status === "cancelled")
52262
+ return { accepted: true, detail: "already cancelled (no attempt launched)" };
52263
+ const cancelled = await this.stateMachine.cancel(runId2);
52264
+ return cancelled.ok ? { accepted: true, detail: "cancelled (no attempt launched)" } : { accepted: false, detail: cancelled.reason };
52265
+ }
52266
+ if (!this.cancellationReceipt(await this.receipts.get(runId2, current.attemptId), run.admission, current)) {
52267
+ return { accepted: false, detail: "matching cancellation receipt is unavailable or contradictory" };
52163
52268
  }
52164
- const taskArn = await this.resolveTaskArn(run.admission, current);
52165
- if (taskArn) {
52269
+ const observation = await this.reconcile(run.admission, current);
52270
+ if (observation.kind !== "already-launched" && observation.kind !== "previous-terminal") {
52271
+ return { accepted: false, detail: "task state unknown; cancellation is not yet confirmed" };
52272
+ }
52273
+ const taskArn = observation.kind === "already-launched" ? observation.taskId : (await this.store.listAttempts(runId2)).find((row) => row.attemptId === current.attemptId)?.taskId ?? null;
52274
+ if (!taskArn)
52275
+ return { accepted: false, detail: "stopped task identity is not confirmed" };
52276
+ if (observation.kind === "already-launched") {
52166
52277
  try {
52167
- await this.client.stopTask(taskArn);
52278
+ await this.client.stopTask(observation.taskId);
52168
52279
  } catch {}
52280
+ const stopped = await this.reconcile(run.admission, { ...current, taskId: observation.taskId });
52281
+ if (stopped.kind !== "previous-terminal") {
52282
+ return { accepted: false, target: observation.taskId, detail: "task stop is not yet confirmed; reconcile before retrying cancellation" };
52283
+ }
52169
52284
  }
52170
- const cancelled = await this.stateMachine.cancel(runId2);
52171
- if (!cancelled.ok)
52172
- return { accepted: false, detail: cancelled.reason };
52173
- await this.writeCancellationReceipt(run.admission, current, taskArn);
52174
- return { accepted: true, target: taskArn ?? undefined, detail: "cancelled and fenced" };
52285
+ if (run.status !== "cancelled") {
52286
+ const cancelled = await this.stateMachine.cancel(runId2);
52287
+ if (!cancelled.ok)
52288
+ return { accepted: false, detail: cancelled.reason };
52289
+ }
52290
+ try {
52291
+ await this.writeCancellationReceipt(run.admission, current, taskArn);
52292
+ } catch {
52293
+ return { accepted: false, target: taskArn, detail: "task stopped; cancellation receipt is not yet confirmed" };
52294
+ }
52295
+ return { accepted: true, target: taskArn, detail: "cancelled after confirmed task stop and receipt" };
52175
52296
  }
52176
52297
  async launchAttempt(runId2) {
52177
52298
  const run = await this.store.getRun(runId2);
@@ -52182,16 +52303,10 @@ class EcsDispatcher {
52182
52303
  }
52183
52304
  const attempts = await this.store.listAttempts(runId2);
52184
52305
  const previous = attempts[attempts.length - 1];
52185
- if (previous && previous.status !== "terminal" && !isProvenAbsentOrTerminal(previous.launchState)) {
52186
- const reconciled = await this.reconcile(run.admission, previous);
52187
- if (reconciled.kind === "already-launched" || reconciled.kind === "previous-terminal") {
52188
- return reconciled;
52189
- }
52190
- if (reconciled.kind === "ambiguous") {
52191
- return reconciled;
52192
- }
52306
+ if (previous) {
52307
+ return this.reconcile(run.admission, previous);
52193
52308
  }
52194
- const attemptNumber = previous ? previous.attemptNumber + 1 : 1;
52309
+ const attemptNumber = 1;
52195
52310
  const attempt = await this.store.createAttempt({ runId: runId2, attemptNumber });
52196
52311
  const claimed = await this.stateMachine.claim({
52197
52312
  runId: runId2,
@@ -52226,68 +52341,43 @@ class EcsDispatcher {
52226
52341
  } catch {
52227
52342
  await this.store.recordLaunchState({ runId: runId2, attemptId: attempt.attemptId, launchState: "ambiguous" });
52228
52343
  const reconciled = await this.reconcile(run.admission, intent.attempt);
52229
- if (reconciled.kind === "already-launched")
52230
- return reconciled;
52231
- return reconciled.kind === "previous-terminal" ? reconciled : { kind: "launch-failed-absent", attemptId: attempt.attemptId };
52344
+ return reconciled;
52232
52345
  }
52233
52346
  await this.store.recordLaunchState({ runId: runId2, attemptId: attempt.attemptId, launchState: "launched", taskId: result.taskArn });
52234
52347
  return { kind: "launched", attemptId: attempt.attemptId, taskId: result.taskArn };
52235
52348
  }
52236
52349
  async reconcile(admission, attempt) {
52237
- if (attempt.taskId) {
52238
- let states2;
52350
+ const ambiguous = () => ({ kind: "ambiguous", attemptId: attempt.attemptId });
52351
+ let taskArn = attempt.taskId;
52352
+ if (!taskArn) {
52353
+ if (!attempt.startedBy)
52354
+ return ambiguous();
52355
+ let taskArns;
52239
52356
  try {
52240
- states2 = await this.client.describeTasks([attempt.taskId]);
52357
+ taskArns = await this.client.listTasksByStartedBy(attempt.startedBy);
52241
52358
  } catch {
52242
- return { kind: "ambiguous", attemptId: attempt.attemptId };
52359
+ return ambiguous();
52243
52360
  }
52244
- const live2 = states2.filter((state) => !TERMINAL_TASK_STATUSES.has(state.lastStatus));
52245
- if (live2.length > 0) {
52246
- return { kind: "already-launched", attemptId: attempt.attemptId, taskId: attempt.taskId };
52247
- }
52248
- await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "terminal" });
52249
- return { kind: "previous-terminal", attemptId: attempt.attemptId };
52250
- }
52251
- const token = attempt.startedBy;
52252
- if (!token) {
52253
- await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "absent" });
52254
- return { kind: "launch-failed-absent", attemptId: attempt.attemptId };
52255
- }
52256
- let taskArns;
52257
- try {
52258
- taskArns = await this.client.listTasksByStartedBy(token);
52259
- } catch {
52260
- return { kind: "ambiguous", attemptId: attempt.attemptId };
52261
- }
52262
- if (taskArns.length === 0) {
52263
- await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "absent" });
52264
- return { kind: "launch-failed-absent", attemptId: attempt.attemptId };
52361
+ if (taskArns.length !== 1 || !taskArns[0])
52362
+ return ambiguous();
52363
+ taskArn = taskArns[0];
52265
52364
  }
52266
52365
  let states;
52267
52366
  try {
52268
- states = await this.client.describeTasks(taskArns);
52367
+ states = await this.client.describeTasks([taskArn]);
52269
52368
  } catch {
52270
- return { kind: "ambiguous", attemptId: attempt.attemptId };
52369
+ return ambiguous();
52271
52370
  }
52272
- const live = states.filter((state) => !TERMINAL_TASK_STATUSES.has(state.lastStatus));
52273
- if (live.length > 0) {
52274
- const taskId = live[0].taskArn;
52275
- await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "launched", taskId });
52276
- return { kind: "already-launched", attemptId: attempt.attemptId, taskId };
52371
+ const state = observedTask(states, taskArn);
52372
+ if (!state)
52373
+ return ambiguous();
52374
+ if (state.lastStatus !== "STOPPED") {
52375
+ await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "launched", taskId: taskArn });
52376
+ return { kind: "already-launched", attemptId: attempt.attemptId, taskId: taskArn };
52277
52377
  }
52278
- await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "terminal", taskId: taskArns[0] });
52378
+ await this.store.recordLaunchState({ runId: admission.runId, attemptId: attempt.attemptId, launchState: "terminal", taskId: taskArn });
52279
52379
  return { kind: "previous-terminal", attemptId: attempt.attemptId };
52280
52380
  }
52281
- async resolveTaskArn(admission, attempt) {
52282
- if (attempt.taskId)
52283
- return attempt.taskId;
52284
- if (attempt.launchState === "launching" || attempt.launchState === "ambiguous") {
52285
- const outcome = await this.reconcile(admission, attempt);
52286
- if (outcome.kind === "already-launched")
52287
- return outcome.taskId;
52288
- }
52289
- return null;
52290
- }
52291
52381
  runTaskInput(admission, attempt, clientToken, startedBy, requestDigest) {
52292
52382
  const limits = admission.limits;
52293
52383
  const cpuUnits = Math.max(256, Math.round(limits.maxCpuUnits / 256) * 256);
@@ -52312,81 +52402,103 @@ class EcsDispatcher {
52312
52402
  ]
52313
52403
  };
52314
52404
  }
52315
- async writeCancellationReceipt(admission, attempt, taskArn) {
52316
- const launch = await this.receipts.get(admission.runId, attempt.attemptId);
52317
- if (!launch) {
52318
- await this.receipts.recordLaunch({
52319
- admission,
52320
- attempt: { ...attempt, clientToken: attempt.clientToken ?? clientTokenFor(admission.runId, attempt.attemptId), requestDigest: attempt.requestDigest ?? "", startedBy: attempt.startedBy ?? "" },
52321
- taskId: taskArn,
52322
- launchedAt: this.now().toISOString()
52323
- });
52405
+ cancellationReceipt(receipt, admission, attempt) {
52406
+ try {
52407
+ return !!receipt && receipt.runId === admission.runId && receipt.attemptId === attempt.attemptId && !!attempt.clientToken && receipt.clientToken === attempt.clientToken && !!attempt.requestDigest && receipt.requestDigest === attempt.requestDigest && !!attempt.startedBy && receipt.startedBy === attempt.startedBy && receipt.bundleDigest === admission.bundleDigest && receipt.runtimeImageDigest === admission.runtimeImageDigest && receipt.dependencyLayerTag === admission.dependencyLayerTag && canonicalJson(receipt.policy) === canonicalJson(admission.policy) && canonicalJson(receipt.limits) === canonicalJson(admission.limits) && (receipt.taskId === null || receipt.taskId === attempt.taskId) && (receipt.status === null && receipt.completedAt === null || receipt.status === "cancelled" && typeof receipt.completedAt === "string" && Number.isFinite(Date.parse(receipt.completedAt)));
52408
+ } catch {
52409
+ return false;
52324
52410
  }
52325
- await this.receipts.finalize({
52326
- runId: admission.runId,
52327
- attemptId: attempt.attemptId,
52328
- status: "cancelled",
52329
- exitCode: null,
52330
- completedAt: this.now().toISOString()
52331
- });
52332
52411
  }
52333
- }
52334
- function isProvenAbsentOrTerminal(launchState) {
52335
- return launchState === "absent" || launchState === "terminal";
52336
- }
52337
- function createAwsEcsClient(region) {
52338
- const client = new ECSClient({ region });
52412
+ async writeCancellationReceipt(admission, attempt, taskArn) {
52413
+ const existing = await this.receipts.get(admission.runId, attempt.attemptId);
52414
+ if (!this.cancellationReceipt(existing, admission, { ...attempt, taskId: taskArn }))
52415
+ throw Error("Cancellation receipt authority unavailable");
52416
+ const completedAt = existing.completedAt ?? this.now().toISOString();
52417
+ const expected = { ...existing, status: "cancelled", completedAt, exitCode: existing.status === "cancelled" ? existing.exitCode : null };
52418
+ const run = await this.store.getRun(admission.runId);
52419
+ if (existing.status !== "cancelled" || run?.status !== "cancelled" || run.terminalReceiptId !== attempt.attemptId) {
52420
+ await this.receipts.finalize({ runId: admission.runId, attemptId: attempt.attemptId, status: "cancelled", exitCode: expected.exitCode, completedAt });
52421
+ }
52422
+ const persisted = await this.receipts.get(admission.runId, attempt.attemptId);
52423
+ const finalized = await this.store.getRun(admission.runId);
52424
+ if (!persisted || canonicalJson(persisted) !== canonicalJson(expected) || finalized?.status !== "cancelled" || finalized.terminalReceiptId !== attempt.attemptId) {
52425
+ throw Error("Cancellation receipt persistence is unconfirmed");
52426
+ }
52427
+ }
52428
+ }
52429
+ function createAwsEcsClient(region, options = {}) {
52430
+ if (options.cluster !== undefined && !validText(options.cluster))
52431
+ throw Error("An explicit nonempty ECS cluster is required");
52432
+ const client = options.transport ?? new ECSClient({ region });
52433
+ let boundCluster = options.cluster;
52434
+ const cluster = (requested) => {
52435
+ const next = requested ?? boundCluster ?? "default";
52436
+ if (!validText(next) || boundCluster !== undefined && next !== boundCluster)
52437
+ throw Error("ECS client cluster binding changed");
52438
+ return boundCluster ??= next;
52439
+ };
52440
+ const response = (value) => {
52441
+ if (!value || typeof value !== "object" || Array.isArray(value))
52442
+ throw Error("Incomplete ECS response");
52443
+ const result = value;
52444
+ if (result.failures !== undefined && (!Array.isArray(result.failures) || result.failures.length > 0))
52445
+ throw Error("ECS response contains failures");
52446
+ return result;
52447
+ };
52339
52448
  return {
52340
52449
  async runTask(input) {
52341
- const response = await client.send(new RunTaskCommand({
52342
- cluster: input.cluster,
52450
+ const result = response(await client.send(new RunTaskCommand({
52451
+ cluster: cluster(input.cluster),
52343
52452
  taskDefinition: input.taskDefinition,
52344
52453
  launchType: input.launchType,
52345
52454
  clientToken: input.clientToken,
52346
52455
  startedBy: input.startedBy,
52347
- networkConfiguration: {
52348
- awsvpcConfiguration: {
52349
- subnets: input.subnets,
52350
- securityGroups: input.securityGroups,
52351
- assignPublicIp: "DISABLED"
52352
- }
52353
- },
52354
- overrides: {
52355
- containerOverrides: [
52356
- {
52357
- name: input.containerName,
52358
- environment: input.environment,
52359
- cpu: Number(input.cpu),
52360
- memory: Number(input.memory)
52361
- }
52362
- ]
52363
- }
52364
- }));
52365
- const taskArn = response.tasks?.[0]?.taskArn ?? null;
52366
- if (!taskArn) {
52367
- const failure = response.failures?.[0];
52368
- throw new Error(`RunTask refused: ${failure?.reason ?? "no task and no failure detail"}`);
52369
- }
52370
- return { taskArn };
52456
+ networkConfiguration: { awsvpcConfiguration: { subnets: input.subnets, securityGroups: input.securityGroups, assignPublicIp: "DISABLED" } },
52457
+ overrides: { containerOverrides: [{ name: input.containerName, environment: input.environment, cpu: Number(input.cpu), memory: Number(input.memory) }] }
52458
+ })));
52459
+ if (!Array.isArray(result.tasks) || result.tasks.length !== 1 || !validText(result.tasks[0]?.taskArn))
52460
+ throw Error("RunTask did not return one exact task");
52461
+ return { taskArn: result.tasks[0].taskArn };
52371
52462
  },
52372
52463
  async listTasksByStartedBy(startedBy) {
52373
- const response = await client.send(new ListTasksCommand({ startedBy }));
52374
- return response.taskArns ?? [];
52464
+ const configuredCluster = cluster(), arns = [], tokens = new Set;
52465
+ let nextToken;
52466
+ for (let page = 0;page < 20; page++) {
52467
+ const result = response(await client.send(new ListTasksCommand({ cluster: configuredCluster, startedBy, ...nextToken ? { nextToken } : {} })));
52468
+ if (!Array.isArray(result.taskArns) || result.taskArns.length > 100 || !result.taskArns.every(validText))
52469
+ throw Error("Incomplete ECS task listing");
52470
+ arns.push(...result.taskArns);
52471
+ if (new Set(arns).size !== arns.length)
52472
+ throw Error("Repeated ECS task identity in listing");
52473
+ if (result.nextToken === undefined)
52474
+ return arns;
52475
+ if (!validText(result.nextToken) || tokens.has(result.nextToken))
52476
+ throw Error("Invalid ECS pagination cursor");
52477
+ tokens.add(result.nextToken);
52478
+ nextToken = result.nextToken;
52479
+ }
52480
+ throw Error("ECS task listing exceeded the bounded page limit");
52375
52481
  },
52376
52482
  async describeTasks(taskArns) {
52377
- const response = await client.send(new DescribeTasksCommand({ tasks: taskArns }));
52378
- return (response.tasks ?? []).map((task) => ({
52379
- taskArn: task.taskArn ?? "",
52380
- lastStatus: task.lastStatus ?? "UNKNOWN",
52381
- stopCode: task.stopCode ?? null,
52382
- exitCode: task.containers?.[0]?.exitCode ?? null
52383
- }));
52483
+ if (!taskArns.length || taskArns.length > 100 || !taskArns.every(validText) || new Set(taskArns).size !== taskArns.length)
52484
+ throw Error("Invalid ECS task identities");
52485
+ const result = response(await client.send(new DescribeTasksCommand({ cluster: cluster(), tasks: taskArns })));
52486
+ if (!Array.isArray(result.tasks) || result.tasks.length !== taskArns.length || new Set(result.tasks.map((task) => task?.taskArn)).size !== taskArns.length || result.tasks.some((task) => !taskArns.includes(task?.taskArn) || !validText(task?.lastStatus)))
52487
+ throw Error("Incomplete ECS task descriptions");
52488
+ return result.tasks.map((task) => ({ taskArn: task.taskArn, lastStatus: task.lastStatus, stopCode: task.stopCode ?? null, exitCode: task.containers?.[0]?.exitCode ?? null }));
52384
52489
  },
52385
52490
  async stopTask(taskArn) {
52386
- await client.send(new StopTaskCommand({ task: taskArn }));
52491
+ if (!validText(taskArn))
52492
+ throw Error("Invalid ECS task identity");
52493
+ const result = response(await client.send(new StopTaskCommand({ cluster: cluster(), task: taskArn })));
52494
+ if (result.task?.taskArn !== taskArn || !validText(result.task?.lastStatus))
52495
+ throw Error("Incomplete ECS stop response");
52387
52496
  }
52388
52497
  };
52389
52498
  }
52499
+ function validText(value) {
52500
+ return typeof value === "string" && value.length > 0 && value.trim() === value && value.length <= 2048;
52501
+ }
52390
52502
  // src/sdk/execution/dispatchers/e2b.ts
52391
52503
  var E2B_LANE_STATUS = "e2b-lane-todo-infinity";
52392
52504
 
@@ -53078,8 +53190,17 @@ function creditCount(value) {
53078
53190
  }
53079
53191
  return value;
53080
53192
  }
53193
+ function runQuoteReceipt(value) {
53194
+ if (value === undefined)
53195
+ return;
53196
+ if (typeof value !== "string" || !value.length || Buffer.byteLength(value, "utf8") > 4096) {
53197
+ throw new Error("Invalid quote receipt");
53198
+ }
53199
+ return value;
53200
+ }
53081
53201
  function parseRemoteRunQuote(value) {
53082
53202
  const quote = object(value);
53203
+ runQuoteReceipt(quote.quoteReceipt);
53083
53204
  const pricing = object(quote.pricing);
53084
53205
  if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
53085
53206
  throw new Error("Invalid quoted skill");
@@ -53226,6 +53347,69 @@ function parseUpdatedWorkspace(value) {
53226
53347
  return { organization: { id: organization2.id, slug: organization2.slug, name: organization2.name } };
53227
53348
  }
53228
53349
 
53350
+ // src/lib/remote-quote-errors.ts
53351
+ var quoteUnavailableMessages = Object.freeze({
53352
+ HOSTED_PROVIDER_UNAVAILABLE: "Hosted execution is temporarily unavailable on this Skills instance.",
53353
+ HOSTED_CONNECTORS_UNAVAILABLE: "Hosted connector execution is unavailable on this Skills instance.",
53354
+ SKILL_IMPLEMENTATION_UNAVAILABLE: "This skill has no hosted execution implementation.",
53355
+ HOSTED_PRICING_UNAVAILABLE: "Hosted execution is unavailable while this skill's pricing is reviewed.",
53356
+ RUNTIME_ALLOWLIST_REQUIRED: "Hosted execution is unavailable until this Skills instance enables its skill catalog.",
53357
+ RUNTIME_SKILL_NOT_ALLOWED: "This skill is not enabled for hosted execution on this Skills instance."
53358
+ });
53359
+ async function readQuoteUnavailableCode(response) {
53360
+ const maximum = 16 * 1024;
53361
+ const length = response.headers.get("content-length");
53362
+ if (response.status !== 503 || response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json" || length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
53363
+ response.body?.cancel().catch(() => {});
53364
+ return null;
53365
+ }
53366
+ const reader = response.body?.getReader();
53367
+ if (!reader)
53368
+ return null;
53369
+ let timer;
53370
+ let deadlineExceeded = false;
53371
+ const expired = Symbol("quote body deadline");
53372
+ const deadline = new Promise((resolve4) => {
53373
+ timer = setTimeout(() => {
53374
+ deadlineExceeded = true;
53375
+ resolve4(expired);
53376
+ reader.cancel().catch(() => {});
53377
+ }, 1500);
53378
+ });
53379
+ const chunks = [];
53380
+ let size = 0;
53381
+ try {
53382
+ while (true) {
53383
+ const next = await Promise.race([reader.read(), deadline]);
53384
+ if (next === expired || deadlineExceeded)
53385
+ return null;
53386
+ if (next.done)
53387
+ break;
53388
+ size += next.value.byteLength;
53389
+ if (size > maximum)
53390
+ return null;
53391
+ chunks.push(next.value);
53392
+ }
53393
+ const bytes = new Uint8Array(size);
53394
+ let offset = 0;
53395
+ for (const chunk of chunks) {
53396
+ bytes.set(chunk, offset);
53397
+ offset += chunk.byteLength;
53398
+ }
53399
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
53400
+ if (!value || typeof value !== "object" || Array.isArray(value))
53401
+ return null;
53402
+ const code = value.code;
53403
+ return typeof code === "string" && Object.hasOwn(quoteUnavailableMessages, code) ? code : null;
53404
+ } catch {
53405
+ return null;
53406
+ } finally {
53407
+ clearTimeout(timer);
53408
+ reader.cancel().catch(() => {});
53409
+ reader.releaseLock();
53410
+ }
53411
+ }
53412
+
53229
53413
  // src/lib/remote-client.ts
53230
53414
  class RemoteRouteUnsupportedError extends Error {
53231
53415
  path;
@@ -53251,6 +53435,18 @@ class RemoteRequestError extends Error {
53251
53435
  }
53252
53436
  }
53253
53437
 
53438
+ class RemoteQuoteUnavailableError extends RemoteRequestError {
53439
+ code;
53440
+ constructor(path, code) {
53441
+ super(path, 503);
53442
+ this.code = code;
53443
+ if (!Object.hasOwn(quoteUnavailableMessages, code))
53444
+ throw new Error("Unknown quote refusal code");
53445
+ this.name = "RemoteQuoteUnavailableError";
53446
+ this.message = quoteUnavailableMessages[code];
53447
+ }
53448
+ }
53449
+
53254
53450
  class RemoteWorkspaceMemberError extends RemoteRequestError {
53255
53451
  code;
53256
53452
  constructor(path, code) {
@@ -53289,7 +53485,7 @@ class RemoteSkillsClient {
53289
53485
  this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
53290
53486
  }
53291
53487
  async request(path, options) {
53292
- return fetch(`${this.apiUrl}${path}`, {
53488
+ return fetch(skillsApiRequestUrl(this.apiUrl, path), {
53293
53489
  ...options,
53294
53490
  redirect: "error",
53295
53491
  credentials: "omit",
@@ -53312,6 +53508,11 @@ class RemoteSkillsClient {
53312
53508
  throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
53313
53509
  }
53314
53510
  if (!response.ok) {
53511
+ if (opts.quoteRefusal && options?.method === "POST" && /^\/api\/v1\/skills\/[^/?#]+\/quote$/.test(routePath) && response.status === 503) {
53512
+ const code = await readQuoteUnavailableCode(response);
53513
+ if (code)
53514
+ throw new RemoteQuoteUnavailableError(routePath, code);
53515
+ }
53315
53516
  if (path === "/api/v1/billing/checkout" && options?.method === "POST" && response.status === 503 && await responseBodyCarriesCode(response, ["SUBSCRIPTION_CHECKOUT_UNAVAILABLE"])) {
53316
53517
  throw new RemoteCapabilityUnavailableError;
53317
53518
  }
@@ -53344,6 +53545,7 @@ class RemoteSkillsClient {
53344
53545
  return { status: res.status, body };
53345
53546
  }
53346
53547
  async submitRun(slug, input, args, approval = {}) {
53548
+ const quoteReceipt = runQuoteReceipt(approval.quoteReceipt);
53347
53549
  if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
53348
53550
  throw new Error("Idempotency key must be 1-128 URL-safe characters");
53349
53551
  if (approval.maxCostCents !== undefined)
@@ -53360,16 +53562,21 @@ class RemoteSkillsClient {
53360
53562
  ...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
53361
53563
  ...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
53362
53564
  ...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
53363
- ...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
53565
+ ...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {},
53566
+ ...quoteReceipt !== undefined ? { quoteReceipt } : {}
53364
53567
  })
53365
53568
  });
53569
+ if (!res.ok) {
53570
+ res.body?.cancel().catch(() => {});
53571
+ throw new RemoteRequestError(`/api/v1/runs/${encodeURIComponent(slug)}`, res.status);
53572
+ }
53366
53573
  return normalizeRemoteSkillRunContract(await res.json(), slug);
53367
53574
  }
53368
- async quoteRun(slug, input = {}, args = []) {
53575
+ async quoteRun(slug, input = {}, args = [], files) {
53369
53576
  const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
53370
53577
  method: "POST",
53371
- body: JSON.stringify({ input, args })
53372
- });
53578
+ body: JSON.stringify({ input, args, ...files === undefined ? {} : { files } })
53579
+ }, { quoteRefusal: true });
53373
53580
  return parseRemoteRunQuote(await response.json());
53374
53581
  }
53375
53582
  getCapabilities() {
@@ -53384,17 +53591,24 @@ class RemoteSkillsClient {
53384
53591
  return this.capabilities;
53385
53592
  }
53386
53593
  async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
53594
+ runQuoteReceipt(approval.quoteReceipt);
53595
+ ({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
53387
53596
  const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
53388
53597
  if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
53389
53598
  throw new Error("Credit approval fields disagree");
53390
- const quote = await this.quoteRun(slug, input, args);
53391
- if (quote.pricing.costCents > maximum)
53599
+ const quote = approval.quoteReceipt === undefined ? await this.quoteRun(slug, input, args, approval.inputFiles?.length ? approval.inputFiles : undefined) : undefined;
53600
+ if (quote && quote.pricing.costCents > maximum)
53392
53601
  throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
53393
53602
  const capabilities = await this.getCapabilities();
53394
53603
  if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
53395
53604
  throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
53396
53605
  }
53397
- return this.submitRun(quote.skill, input, args, { ...approval, maxCredits: maximum, maxCostCents: maximum });
53606
+ return this.submitRun(quote?.skill ?? slug, input, args, {
53607
+ ...approval,
53608
+ maxCredits: maximum,
53609
+ maxCostCents: maximum,
53610
+ ...(quote?.quoteReceipt ?? approval.quoteReceipt) === undefined ? {} : { quoteReceipt: quote?.quoteReceipt ?? approval.quoteReceipt }
53611
+ });
53398
53612
  }
53399
53613
  async getIdentity() {
53400
53614
  return (await this.requestNewRoute("/api/auth/whoami")).json();
@@ -53682,6 +53896,10 @@ class RemoteSkillsClient {
53682
53896
  return { id: artifactId2, fileName: String(artifact.fileName ?? artifactId2), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
53683
53897
  }
53684
53898
  async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
53899
+ runQuoteReceipt(approval.quoteReceipt);
53900
+ ({ input, args, approval } = JSON.parse(JSON.stringify({ input, args, approval })));
53901
+ describeRemoteFiles(files);
53902
+ files = files.map((file) => ({ name: file.name, contentType: file.contentType, bytes: new Uint8Array(file.bytes) }));
53685
53903
  const inputFiles = describeRemoteFiles(files);
53686
53904
  if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
53687
53905
  throw new Error("The configured server does not support input uploads");
@@ -53745,7 +53963,7 @@ class RemoteSkillsClient {
53745
53963
  const headers = { Authorization: `Bearer ${this.apiKey}` };
53746
53964
  if (ifMatch)
53747
53965
  headers["If-Match"] = ifMatch;
53748
- return fetch(`${this.apiUrl}/api/v1/skills`, {
53966
+ return fetch(skillsApiRequestUrl(this.apiUrl, "/api/v1/skills"), {
53749
53967
  method: "POST",
53750
53968
  headers,
53751
53969
  body: form,
@@ -54030,7 +54248,7 @@ async function requestInvitationEmail(origin, action, input) {
54030
54248
  }
54031
54249
  const body = JSON.stringify({ invitationId: value.invitationId, token: value.token, challengeId: value.challengeId, ...action === "accept" ? { code: value.code } : {} });
54032
54250
  try {
54033
- const response = await fetch(`${target}/api/v1/account/invitations/email-${action}`, {
54251
+ const response = await fetch(skillsApiRequestUrl(target, `/api/v1/account/invitations/email-${action}`), {
54034
54252
  method: "POST",
54035
54253
  headers: { "Content-Type": "application/json" },
54036
54254
  body,
@@ -54061,6 +54279,312 @@ async function requestInvitationEmail(origin, action, input) {
54061
54279
  throw new RemoteInvitationEmailUnconfirmedError(action);
54062
54280
  }
54063
54281
 
54282
+ // src/lib/remote-private-publications.ts
54283
+ import { createHash as createHash14 } from "crypto";
54284
+ var PRIVATE_PUBLICATION_MAX_BYTES = 16 * 1024 * 1024;
54285
+ var failures = {
54286
+ INVALID_REQUEST: [400, "The publication request is invalid."],
54287
+ SESSION_EXPIRED: [401, "Sign in again to manage this publication."],
54288
+ ACCOUNT_UNAVAILABLE: [403, "The account is unavailable."],
54289
+ INTERACTIVE_SESSION_REQUIRED: [403, "An interactive account session is required."],
54290
+ PUBLICATION_FORBIDDEN: [403, "This session cannot manage the publication."],
54291
+ PUBLICATION_ENTITLEMENT_REQUIRED: [403, "The workspace is not entitled to publish private skills."],
54292
+ PUBLICATION_UNAVAILABLE: [404, "The publication is unavailable to this session."],
54293
+ MANIFEST_NAME_MISMATCH: [409, "The manifest name does not match the selected skill."],
54294
+ IDEMPOTENCY_CONFLICT: [409, "This request key already identifies different publication bytes. Use the saved recovery directory."],
54295
+ CURRENT_VERSION_CHANGED: [409, "The current version changed. Inspect it before explicitly starting another publication."],
54296
+ VERSION_EXISTS: [409, "This version already exists."],
54297
+ VERSION_RESERVED: [409, "This version is reserved by another publication."],
54298
+ PUBLICATION_COMMITTED: [409, "The publication is already committed."],
54299
+ PUBLICATION_UPLOAD_UNAVAILABLE: [409, "This publication cannot receive another upload."],
54300
+ PUBLICATION_LIMIT: [429, "The workspace publication limit has been reached."],
54301
+ PUBLICATION_BUSY: [503, "The publication is busy. Reconcile the saved intent before retrying."],
54302
+ PUBLICATION_UNCERTAIN: [503, "The publication outcome is uncertain. Reconcile the saved intent."],
54303
+ PUBLICATION_CAPABILITY_UNAVAILABLE: [503, "Private publishing is not enabled on this server."],
54304
+ PUBLICATION_SIGNING_UNAVAILABLE: [503, "Upload authorization is temporarily unavailable. Keep the same intent."]
54305
+ };
54306
+
54307
+ class PrivatePublicationError extends Error {
54308
+ code;
54309
+ uncertain;
54310
+ status;
54311
+ constructor(code, message, uncertain = false, status) {
54312
+ super(message);
54313
+ this.code = code;
54314
+ this.uncertain = uncertain;
54315
+ this.status = status;
54316
+ this.name = "PrivatePublicationError";
54317
+ }
54318
+ }
54319
+ var bad = () => {
54320
+ throw new PrivatePublicationError("INVALID_PUBLICATION_INPUT", "Invalid publication input or recovery data.");
54321
+ };
54322
+ var invalid3 = () => {
54323
+ throw new PrivatePublicationError("INVALID_PUBLICATION_RESPONSE", "The server returned an invalid publication result.");
54324
+ };
54325
+ var publicationUuid = (v2) => typeof v2 === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(v2);
54326
+ var hash = (v2) => typeof v2 === "string" && /^[a-f0-9]{64}$/.test(v2);
54327
+ var record5 = (v2) => !!v2 && typeof v2 === "object" && !Array.isArray(v2);
54328
+ var exact = (v2, keys) => record5(v2) && Object.keys(v2).sort().join(",") === keys.sort().join(",");
54329
+ var date = (v2) => typeof v2 === "string" && /^\d{4}-\d\d-\d\dT[0-9:.]+(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(v2) && Number.isFinite(Date.parse(v2));
54330
+ var publicationSha256 = (bytes) => createHash14("sha256").update(bytes).digest("hex");
54331
+ function checkedPublicationDeclaration(value) {
54332
+ if (!exact(value, ["idempotencyKey", "version", "expectedCurrentVersionId", "manifestText", "archiveSha256", "archiveByteSize"]) || !publicationUuid(value.idempotencyKey) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || typeof value.manifestText !== "string" || Buffer.byteLength(value.manifestText) > 16384 || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES)
54333
+ return bad();
54334
+ try {
54335
+ const manifest = JSON.parse(value.manifestText);
54336
+ if (!record5(manifest) || manifest.version !== value.version || validatePortableManifestContract(manifest, { strict: true }).length || !hash(manifest.provenance?.content_hash))
54337
+ return bad();
54338
+ } catch {
54339
+ return bad();
54340
+ }
54341
+ return Object.freeze({ ...value });
54342
+ }
54343
+ function checkedPublicationView(value, skillId, intentId, declaration) {
54344
+ if (!exact(value, ["id", "skillId", "version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize", "state", "expiresAt", "createdAt", "versionId"]) || !publicationUuid(value.id) || value.skillId !== skillId || intentId !== undefined && value.id !== intentId || typeof value.version !== "string" || !value.version || value.version.length > 128 || /[\p{Cc}\p{Cs}]/u.test(value.version) || !(value.expectedCurrentVersionId === null || publicationUuid(value.expectedCurrentVersionId)) || !hash(value.archiveSha256) || !Number.isSafeInteger(value.archiveByteSize) || Number(value.archiveByteSize) < 1 || Number(value.archiveByteSize) > PRIVATE_PUBLICATION_MAX_BYTES || typeof value.state !== "string" || !["awaiting_upload", "queued", "verifying", "needs_attention", "committed", "rejected", "cancelled", "expired"].includes(value.state) || !date(value.expiresAt) || !date(value.createdAt) || !(value.versionId === null || publicationUuid(value.versionId)) || value.state === "committed" && value.versionId === null)
54345
+ return invalid3();
54346
+ if (declaration && ["version", "expectedCurrentVersionId", "archiveSha256", "archiveByteSize"].some((k4) => value[k4] !== declaration[k4]))
54347
+ return invalid3();
54348
+ return Object.freeze({ ...value });
54349
+ }
54350
+ async function boundedJson(url, init, token, mutation, budget = 15000, signal) {
54351
+ const controller = new AbortController;
54352
+ let reader;
54353
+ let response;
54354
+ const timeout = new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "No confirmed publication result. Inspect the saved intent before retrying.", mutation);
54355
+ let timer;
54356
+ let abort;
54357
+ try {
54358
+ return await Promise.race([(async () => {
54359
+ if (signal?.aborted)
54360
+ throw timeout;
54361
+ response = await fetch(url, {
54362
+ ...init,
54363
+ redirect: "error",
54364
+ credentials: "omit",
54365
+ signal: controller.signal,
54366
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
54367
+ });
54368
+ const length = response.headers.get("content-length");
54369
+ if (length !== null && (!/^\d+$/.test(length) || Number(length) > 65536))
54370
+ return invalid3();
54371
+ reader = response.body?.getReader();
54372
+ const chunks = [];
54373
+ let size = 0;
54374
+ if (reader)
54375
+ while (true) {
54376
+ const part = await reader.read();
54377
+ if (part.done)
54378
+ break;
54379
+ size += part.value.byteLength;
54380
+ if (size > 65536)
54381
+ return invalid3();
54382
+ chunks.push(part.value);
54383
+ }
54384
+ if (controller.signal.aborted)
54385
+ throw timeout;
54386
+ const bytes = Buffer.concat(chunks);
54387
+ let body;
54388
+ try {
54389
+ body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
54390
+ } catch {
54391
+ return invalid3();
54392
+ }
54393
+ if (!response.ok) {
54394
+ const code = record5(body) && typeof body.code === "string" && Object.hasOwn(failures, body.code) ? body.code : null;
54395
+ if (code && failures[code][0] === response.status)
54396
+ throw new PrivatePublicationError(code, failures[code][1], mutation && code === "PUBLICATION_UNCERTAIN", response.status);
54397
+ throw new PrivatePublicationError("PUBLICATION_REQUEST_FAILED", "The publication request was refused. Inspect its status before retrying.", mutation && response.status >= 500, response.status);
54398
+ }
54399
+ return body;
54400
+ })(), new Promise((_, reject) => {
54401
+ abort = () => {
54402
+ controller.abort();
54403
+ reject(timeout);
54404
+ };
54405
+ timer = setTimeout(abort, Math.max(1, Math.min(15000, budget)));
54406
+ signal?.addEventListener("abort", abort, { once: true });
54407
+ })]);
54408
+ } catch (error) {
54409
+ if (error instanceof PrivatePublicationError) {
54410
+ if (mutation && error.code === "INVALID_PUBLICATION_RESPONSE")
54411
+ throw timeout;
54412
+ throw error;
54413
+ }
54414
+ throw timeout;
54415
+ } finally {
54416
+ if (timer)
54417
+ clearTimeout(timer);
54418
+ if (abort)
54419
+ signal?.removeEventListener("abort", abort);
54420
+ controller.abort();
54421
+ if (reader)
54422
+ reader.cancel().catch(() => {});
54423
+ else
54424
+ response?.body?.cancel().catch(() => {});
54425
+ }
54426
+ }
54427
+
54428
+ class RemotePrivatePublicationsClient {
54429
+ apiOrigin;
54430
+ organizationId;
54431
+ userId;
54432
+ membershipId;
54433
+ #token;
54434
+ constructor(apiUrl, session) {
54435
+ const checked = parseWorkspaceSession(session, { userId: session?.user?.id, membershipId: session?.user?.membershipId });
54436
+ if (checked.user.role === "viewer")
54437
+ throw new PrivatePublicationError("PUBLICATION_FORBIDDEN", failures.PUBLICATION_FORBIDDEN[1]);
54438
+ this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
54439
+ this.#token = checked.token;
54440
+ this.organizationId = checked.organization.id;
54441
+ this.userId = checked.user.id;
54442
+ this.membershipId = checked.user.membershipId;
54443
+ Object.freeze(this);
54444
+ }
54445
+ async getCapability(options = {}) {
54446
+ if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 15000))
54447
+ return bad();
54448
+ const response = await boundedJson(skillsApiRequestUrl(this.apiOrigin, "/api/v1/capabilities"), {}, this.#token, false, options.timeoutMs, options.signal);
54449
+ 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)
54451
+ throw new PrivatePublicationError("PUBLICATION_CONTRACT_UNAVAILABLE", "This server does not support the hosted private publication contract.");
54452
+ return Object.freeze({ ...p2 });
54453
+ }
54454
+ async#gate(enabled, options = {}) {
54455
+ if (!(await this.getCapability(options)).enabled && enabled)
54456
+ throw new PrivatePublicationError("PUBLICATION_CAPABILITY_UNAVAILABLE", failures.PUBLICATION_CAPABILITY_UNAVAILABLE[1]);
54457
+ }
54458
+ #path(skillId, intentId) {
54459
+ if (!publicationUuid(skillId) || intentId !== undefined && !publicationUuid(intentId))
54460
+ return bad();
54461
+ return skillsApiRequestUrl(this.apiOrigin, `/api/v1/skills/${skillId}/publication-uploads${intentId ? `/${intentId}` : ""}`);
54462
+ }
54463
+ async#view(path, method, skillId, intentId, declaration, options = {}) {
54464
+ const value = await boundedJson(path, { method, ...method === "GET" ? {} : { body: JSON.stringify(declaration ?? {}) } }, this.#token, method !== "GET", options.timeoutMs, options.signal);
54465
+ try {
54466
+ if (!record5(value) || !Object.keys(value).every((k4) => k4 === "upload" || k4 === "changed") || value.changed !== undefined && typeof value.changed !== "boolean")
54467
+ return invalid3();
54468
+ return checkedPublicationView(value.upload, skillId, intentId, declaration);
54469
+ } catch (error) {
54470
+ if (method !== "GET")
54471
+ throw new PrivatePublicationError("PUBLICATION_UNCONFIRMED", "The publication result could not be confirmed. Reconcile the saved intent before another action.", true);
54472
+ throw error;
54473
+ }
54474
+ }
54475
+ async begin(skillId, input) {
54476
+ const path = this.#path(skillId), declaration = checkedPublicationDeclaration(input);
54477
+ await this.#gate(true);
54478
+ return this.#view(path, "POST", skillId, undefined, declaration);
54479
+ }
54480
+ async get(skillId, intentId, options = {}) {
54481
+ const path = this.#path(skillId, intentId), until = Date.now() + (options.timeoutMs ?? 15000);
54482
+ await this.#gate(false, options);
54483
+ return this.#view(path, "GET", skillId, intentId, undefined, { ...options, timeoutMs: Math.max(1, until - Date.now()) });
54484
+ }
54485
+ async finalize(skillId, intentId) {
54486
+ const path = this.#path(skillId, intentId);
54487
+ await this.#gate(true);
54488
+ return this.#view(`${path}/finalize`, "POST", skillId, intentId);
54489
+ }
54490
+ async cancel(skillId, intentId) {
54491
+ const path = this.#path(skillId, intentId);
54492
+ await this.#gate(false);
54493
+ return this.#view(path, "DELETE", skillId, intentId);
54494
+ }
54495
+ async upload(skillId, intent, bytes) {
54496
+ const captured = checkedPublicationView(intent, skillId), path = this.#path(skillId, captured.id);
54497
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== captured.archiveByteSize)
54498
+ return bad();
54499
+ const owned = Buffer.from(bytes);
54500
+ if (captured.state !== "awaiting_upload" || owned.byteLength !== captured.archiveByteSize || publicationSha256(owned) !== captured.archiveSha256)
54501
+ return bad();
54502
+ await this.#gate(true);
54503
+ const value = await boundedJson(`${path}/upload-url`, { method: "POST", body: "{}" }, this.#token, false);
54504
+ const upload = this.#upload(value, captured);
54505
+ const controller = new AbortController;
54506
+ let timer;
54507
+ const uncertain = () => new PrivatePublicationError("PUBLICATION_UPLOAD_UNCONFIRMED", "Upload acceptance is uncertain. Resume this saved intent to finalize and inspect it; do not upload again.", true);
54508
+ try {
54509
+ await Promise.race([(async () => {
54510
+ const response = await fetch(upload.uploadUrl, { method: "PUT", headers: upload.headers, body: owned, redirect: "error", credentials: "omit", signal: controller.signal });
54511
+ response.body?.cancel().catch(() => {});
54512
+ if (response.status !== 200 || controller.signal.aborted)
54513
+ throw uncertain();
54514
+ })(), new Promise((_, reject) => {
54515
+ timer = setTimeout(() => {
54516
+ controller.abort();
54517
+ reject(uncertain());
54518
+ }, 30000);
54519
+ })]);
54520
+ } catch {
54521
+ throw uncertain();
54522
+ } finally {
54523
+ if (timer)
54524
+ clearTimeout(timer);
54525
+ controller.abort();
54526
+ }
54527
+ }
54528
+ #upload(value, intent) {
54529
+ if (!exact(value, ["upload"]) || !exact(value.upload, ["method", "uploadUrl", "headers", "expiresAt"]))
54530
+ return invalid3();
54531
+ const p2 = value.upload;
54532
+ if (p2.method !== "PUT" || typeof p2.uploadUrl !== "string" || p2.uploadUrl.length > 8192 || /[\x00-\x20\x7f]/.test(p2.uploadUrl) || !date(p2.expiresAt) || Date.parse(p2.expiresAt) - Date.now() < 1000 || Date.parse(p2.expiresAt) - Date.now() > 300000 || Date.parse(p2.expiresAt) > Date.parse(intent.expiresAt) || !exact(p2.headers, ["content-type", "content-length", "x-amz-checksum-sha256", "x-amz-expected-bucket-owner"]) || p2.headers["content-type"] !== "application/gzip" || p2.headers["content-length"] !== String(intent.archiveByteSize) || p2.headers["x-amz-checksum-sha256"] !== Buffer.from(intent.archiveSha256, "hex").toString("base64") || typeof p2.headers["x-amz-expected-bucket-owner"] !== "string" || !/^\d{12}$/.test(p2.headers["x-amz-expected-bucket-owner"]))
54533
+ return invalid3();
54534
+ let url;
54535
+ try {
54536
+ url = new URL(p2.uploadUrl);
54537
+ } catch {
54538
+ return invalid3();
54539
+ }
54540
+ if (url.protocol !== "https:" || url.port || url.username || url.password || url.hash || !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]\.s3\.[a-z]{2}(?:-[a-z]+)+-[1-9]\.amazonaws\.com$/.test(url.hostname) || url.pathname !== `/private-publication-staging/${this.organizationId}/${intent.id}/bundle.tgz` || !url.search || url.searchParams.get("X-Amz-Algorithm") !== "AWS4-HMAC-SHA256" || url.searchParams.get("X-Amz-SignedHeaders") !== "content-length;content-type;host;x-amz-checksum-sha256;x-amz-expected-bucket-owner" || !/^[a-f0-9]{64}$/.test(url.searchParams.get("X-Amz-Signature") ?? ""))
54541
+ return invalid3();
54542
+ const queryKeys = ["X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date", "X-Amz-Expires", "X-Amz-Security-Token", "X-Amz-Signature", "X-Amz-SignedHeaders"];
54543
+ if ([...url.searchParams.keys()].sort().join(",") !== queryKeys.sort().join(","))
54544
+ return invalid3();
54545
+ const issued = url.searchParams.get("X-Amz-Date"), ttl = url.searchParams.get("X-Amz-Expires"), credential = url.searchParams.get("X-Amz-Credential");
54546
+ const timestamp3 = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/.exec(issued);
54547
+ const region = url.hostname.split(".s3.")[1].split(".amazonaws.com")[0];
54548
+ if (!timestamp3 || !/^[1-9]\d{0,2}$/.test(ttl) || Number(ttl) > 300 || !/^[A-Z0-9]{16,128}\//.test(credential) || credential.split("/").slice(1).join("/") !== `${issued.slice(0, 8)}/${region}/s3/aws4_request` || !/^[\x21-\x7e]{1,4096}$/.test(url.searchParams.get("X-Amz-Security-Token")))
54549
+ return invalid3();
54550
+ const issuedAt = Date.parse(`${timestamp3[1]}-${timestamp3[2]}-${timestamp3[3]}T${timestamp3[4]}:${timestamp3[5]}:${timestamp3[6]}Z`);
54551
+ if (!Number.isFinite(issuedAt) || issuedAt > Date.now() + 1000 || issuedAt + Number(ttl) * 1000 !== Date.parse(p2.expiresAt))
54552
+ return invalid3();
54553
+ return { method: "PUT", uploadUrl: url.href, expiresAt: p2.expiresAt, headers: Object.freeze({ ...p2.headers }) };
54554
+ }
54555
+ async wait(skillId, intentId, options = {}) {
54556
+ const timeout = options.timeoutMs ?? 60000;
54557
+ if (!Number.isSafeInteger(timeout) || timeout < 0 || timeout > 300000)
54558
+ return bad();
54559
+ const until = Date.now() + timeout;
54560
+ let previous;
54561
+ while (true) {
54562
+ if (options.signal?.aborted)
54563
+ throw new PrivatePublicationError("PUBLICATION_WAIT_ABORTED", "Stopped waiting. The server publication continues; inspect the saved intent.");
54564
+ let view;
54565
+ try {
54566
+ view = await this.get(skillId, intentId, { timeoutMs: timeout === 0 ? 15000 : Math.max(1, Math.min(15000, until - Date.now())), signal: options.signal });
54567
+ } catch (error) {
54568
+ if (previous && Date.now() >= until && !options.signal?.aborted)
54569
+ return previous;
54570
+ throw error;
54571
+ }
54572
+ previous = view;
54573
+ if (!["queued", "verifying"].includes(view.state) || Date.now() >= until)
54574
+ return view;
54575
+ await new Promise((resolve4) => {
54576
+ const timer = setTimeout(done, Math.min(1000, until - Date.now()));
54577
+ function done() {
54578
+ clearTimeout(timer);
54579
+ options.signal?.removeEventListener("abort", done);
54580
+ resolve4();
54581
+ }
54582
+ options.signal?.addEventListener("abort", done, { once: true });
54583
+ });
54584
+ }
54585
+ }
54586
+ }
54587
+
54064
54588
  // src/lib/remote-auth.ts
54065
54589
  var MAX_ERROR_DETAIL_LENGTH = 200;
54066
54590
 
@@ -54083,10 +54607,11 @@ class HostedApiError extends Error {
54083
54607
  async function requestAuthApi(instance, path, options) {
54084
54608
  const url = normalizeSkillsApiOrigin(instance);
54085
54609
  const safeUrl = url;
54086
- const endpoint = `${(options?.method || "GET").toUpperCase()} ${safeUrl}${path}`;
54610
+ const requestUrl = skillsApiRequestUrl(url, path);
54611
+ const endpoint = `${(options?.method || "GET").toUpperCase()} ${requestUrl}`;
54087
54612
  let res;
54088
54613
  try {
54089
- res = await fetch(`${url}${path}`, {
54614
+ res = await fetch(requestUrl, {
54090
54615
  ...options,
54091
54616
  redirect: "error",
54092
54617
  signal: options?.signal ?? AbortSignal.timeout(15000),
@@ -54101,10 +54626,10 @@ async function requestAuthApi(instance, path, options) {
54101
54626
  const text2 = await res.text();
54102
54627
  const body = text2 ? parseJsonBody(text2) : {};
54103
54628
  if (!res.ok) {
54104
- const record5 = isRecord6(body) ? body : {};
54105
- const detail = typeof record5.detail === "string" ? record5.detail : undefined;
54106
- const error = typeof record5.error === "string" ? record5.error : undefined;
54107
- const code = typeof record5.code === "string" ? record5.code : undefined;
54629
+ const record6 = isRecord6(body) ? body : {};
54630
+ const detail = typeof record6.detail === "string" ? record6.detail : undefined;
54631
+ const error = typeof record6.error === "string" ? record6.error : undefined;
54632
+ const code = typeof record6.code === "string" ? record6.code : undefined;
54108
54633
  throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
54109
54634
  status: res.status,
54110
54635
  code,
@@ -54138,6 +54663,10 @@ class RemoteSkillsAuthClient {
54138
54663
  constructor(apiUrl) {
54139
54664
  this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
54140
54665
  }
54666
+ async openPrivatePublications(email2, code, context) {
54667
+ const origin = this.apiOrigin, captured = workspaceContext(context);
54668
+ return new RemotePrivatePublicationsClient(origin, await this.switchWorkspace(email2, code, captured));
54669
+ }
54141
54670
  requestInvitationEmailChallenge(input) {
54142
54671
  return requestInvitationEmail(this.apiOrigin, "challenge", input);
54143
54672
  }
@@ -54185,9 +54714,10 @@ class RemoteSkillsAuthClient {
54185
54714
  const apiOrigin = this.apiOrigin;
54186
54715
  if (typeof email2 !== "string" || !email2.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
54187
54716
  throw new Error("Fresh email and six-digit verification code are required to manage this account");
54717
+ const requestUrl = skillsApiRequestUrl(apiOrigin, "/api/auth/verify");
54188
54718
  let response;
54189
54719
  try {
54190
- response = await fetch(`${apiOrigin}/api/auth/verify`, {
54720
+ response = await fetch(requestUrl, {
54191
54721
  method: "POST",
54192
54722
  redirect: "error",
54193
54723
  credentials: "omit",
@@ -54278,6 +54808,242 @@ class RemoteSkillsAuthClient {
54278
54808
  return requestAuthApi(this.apiOrigin, path, options);
54279
54809
  }
54280
54810
  }
54811
+ // src/lib/private-publication-recovery.ts
54812
+ import { constants as constants2, closeSync as closeSync3, fsyncSync, fstatSync as fstatSync3, lstatSync as lstatSync5, mkdirSync as mkdirSync8, openSync as openSync3, readSync as readSync2, realpathSync as realpathSync2, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
54813
+ import { dirname as dirname10, isAbsolute as isAbsolute5, join as join21, resolve as resolve4 } from "path";
54814
+ import { randomUUID as randomUUID6 } from "crypto";
54815
+ var fail = () => {
54816
+ throw new PrivatePublicationError("PUBLICATION_RECOVERY_INVALID", "The recovery directory is invalid or changed. Preserve it and inspect the existing intent; do not start a replacement automatically.");
54817
+ };
54818
+ function safeDirectory(directory) {
54819
+ if (!isAbsolute5(directory) || directory !== resolve4(directory) || realpathSync2(directory) !== directory)
54820
+ return fail();
54821
+ for (let path = directory;; path = dirname10(path)) {
54822
+ const stat = lstatSync5(path);
54823
+ if (!stat.isDirectory() || stat.isSymbolicLink())
54824
+ return fail();
54825
+ if (path === dirname10(path))
54826
+ break;
54827
+ }
54828
+ const own = lstatSync5(directory);
54829
+ if ((own.mode & 63) !== 0 || process.getuid && own.uid !== process.getuid())
54830
+ return fail();
54831
+ return { dev: own.dev, ino: own.ino };
54832
+ }
54833
+ function unchangedDirectory(directory, identity) {
54834
+ const now2 = safeDirectory(directory);
54835
+ if (now2.dev !== identity.dev || now2.ino !== identity.ino)
54836
+ return fail();
54837
+ }
54838
+ function readOwned(directory, name, max) {
54839
+ const identity = safeDirectory(directory), file = join21(directory, name);
54840
+ const fd = openSync3(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
54841
+ try {
54842
+ const stat = fstatSync3(fd);
54843
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > max || stat.size < 1 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
54844
+ return fail();
54845
+ const buffer = Buffer.alloc(Math.min(max, stat.size) + 1);
54846
+ let length = 0;
54847
+ while (length < buffer.length) {
54848
+ const count = readSync2(fd, buffer, length, buffer.length - length, length);
54849
+ if (count === 0)
54850
+ break;
54851
+ length += count;
54852
+ }
54853
+ const bytes = buffer.subarray(0, length), after = fstatSync3(fd);
54854
+ unchangedDirectory(directory, identity);
54855
+ if (bytes.length !== stat.size || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs)
54856
+ return fail();
54857
+ return bytes;
54858
+ } finally {
54859
+ closeSync3(fd);
54860
+ }
54861
+ }
54862
+ function writeOwned(directory, name, bytes) {
54863
+ const fd = openSync3(join21(directory, name), constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
54864
+ try {
54865
+ writeFileSync6(fd, bytes);
54866
+ fsyncSync(fd);
54867
+ } finally {
54868
+ closeSync3(fd);
54869
+ }
54870
+ }
54871
+ function save(directory, value) {
54872
+ const identity = safeDirectory(directory), temporary = `.receipt-${randomUUID6()}.json`;
54873
+ writeOwned(directory, temporary, JSON.stringify(value) + `
54874
+ `);
54875
+ unchangedDirectory(directory, identity);
54876
+ renameSync3(join21(directory, temporary), join21(directory, "receipt.json"));
54877
+ const fd = openSync3(directory, constants2.O_RDONLY | constants2.O_NOFOLLOW);
54878
+ try {
54879
+ fsyncSync(fd);
54880
+ } finally {
54881
+ closeSync3(fd);
54882
+ }
54883
+ }
54884
+ function bind(client, receipt) {
54885
+ if (["apiOrigin", "organizationId", "userId", "membershipId"].some((k4) => client[k4] !== receipt[k4]))
54886
+ throw new PrivatePublicationError("PUBLICATION_IDENTITY_CHANGED", "The fresh session does not match the recovery directory's server, account and membership.");
54887
+ }
54888
+ async function locked(directory, action) {
54889
+ const identity = safeDirectory(directory), file = join21(directory, "operation.lock");
54890
+ let fd;
54891
+ try {
54892
+ fd = openSync3(file, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
54893
+ } catch {
54894
+ throw new PrivatePublicationError("PUBLICATION_RECOVERY_BUSY", "Another operation holds this recovery directory. If it crashed, confirm that process has stopped before explicitly removing operation.lock and resuming.");
54895
+ }
54896
+ const lock = fstatSync3(fd);
54897
+ try {
54898
+ writeFileSync6(fd, JSON.stringify({ pid: process.pid }) + `
54899
+ `);
54900
+ fsyncSync(fd);
54901
+ return await action();
54902
+ } finally {
54903
+ closeSync3(fd);
54904
+ unchangedDirectory(directory, identity);
54905
+ const now2 = lstatSync5(file);
54906
+ if (now2.dev !== lock.dev || now2.ino !== lock.ino || !now2.isFile() || now2.isSymbolicLink())
54907
+ fail();
54908
+ unlinkSync(file);
54909
+ }
54910
+ }
54911
+ function readPrivatePublicationRecovery(directory) {
54912
+ try {
54913
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readOwned(directory, "receipt.json", 65536)));
54914
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== ["contractVersion", "apiOrigin", "organizationId", "userId", "membershipId", "skillId", "declaration", "phase", "intent"].sort().join(",") || value.contractVersion !== 1 || ![value.organizationId, value.userId, value.membershipId, value.skillId].every(publicationUuid) || typeof value.apiOrigin !== "string" || normalizeSkillsApiOrigin(value.apiOrigin) !== value.apiOrigin || !["prepared", "begin_uncertain", "awaiting_upload", "upload_uncertain", "uploaded", "finalize_uncertain", "observed"].includes(value.phase))
54915
+ return fail();
54916
+ value.declaration = checkedPublicationDeclaration(value.declaration);
54917
+ if (value.intent !== null)
54918
+ value.intent = checkedPublicationView(value.intent, value.skillId, undefined, value.declaration);
54919
+ if (value.intent === null && !["prepared", "begin_uncertain"].includes(value.phase))
54920
+ return fail();
54921
+ const bytes = readOwned(directory, "bundle.tgz", PRIVATE_PUBLICATION_MAX_BYTES);
54922
+ if (bytes.length !== value.declaration.archiveByteSize || publicationSha256(bytes) !== value.declaration.archiveSha256)
54923
+ return fail();
54924
+ return { receipt: value, bytes };
54925
+ } catch {
54926
+ return fail();
54927
+ }
54928
+ }
54929
+ async function preparePrivatePublication(client, sourceDirectory, recoveryDirectory, input) {
54930
+ if (!publicationUuid(input.skillId) || !(input.expectedCurrentVersionId === null || publicationUuid(input.expectedCurrentVersionId)) || input.idempotencyKey !== undefined && !publicationUuid(input.idempotencyKey))
54931
+ return fail();
54932
+ const packed = packSkillBundle(sourceDirectory, { maxUnpackedBytes: 32 * 1024 * 1024 });
54933
+ const inspected = await inspectSkillBundle(packed.bytes, { limits: { compressedBytes: PRIVATE_PUBLICATION_MAX_BYTES } });
54934
+ const manifest = inspected.entries.find((entry) => entry.path === "skill.json");
54935
+ if (!manifest || manifest.bytes.length > 16384 || !(await verifyContentHashFromEntries(inspected.entries)).valid)
54936
+ throw new PrivatePublicationError("PUBLICATION_BUNDLE_INVALID", "The skill must have a valid skill.json with its current content hash. Validate the skill before publishing.");
54937
+ const manifestText = new TextDecoder("utf-8", { fatal: true }).decode(manifest.bytes);
54938
+ const declaration = checkedPublicationDeclaration({
54939
+ idempotencyKey: input.idempotencyKey ?? randomUUID6(),
54940
+ version: JSON.parse(manifestText).version,
54941
+ expectedCurrentVersionId: input.expectedCurrentVersionId,
54942
+ manifestText,
54943
+ archiveSha256: inspected.sha256,
54944
+ archiveByteSize: packed.bytes.length
54945
+ });
54946
+ const receipt = {
54947
+ contractVersion: 1,
54948
+ apiOrigin: client.apiOrigin,
54949
+ organizationId: client.organizationId,
54950
+ userId: client.userId,
54951
+ membershipId: client.membershipId,
54952
+ skillId: input.skillId,
54953
+ declaration,
54954
+ phase: "prepared",
54955
+ intent: null
54956
+ };
54957
+ if (!isAbsolute5(recoveryDirectory) || resolve4(recoveryDirectory) !== recoveryDirectory || realpathSync2(dirname10(recoveryDirectory)) !== dirname10(recoveryDirectory))
54958
+ return fail();
54959
+ mkdirSync8(recoveryDirectory, { mode: 448 });
54960
+ safeDirectory(recoveryDirectory);
54961
+ const parent = openSync3(dirname10(recoveryDirectory), constants2.O_RDONLY | constants2.O_NOFOLLOW);
54962
+ try {
54963
+ fsyncSync(parent);
54964
+ } finally {
54965
+ closeSync3(parent);
54966
+ }
54967
+ writeOwned(recoveryDirectory, "bundle.tgz", packed.bytes);
54968
+ save(recoveryDirectory, receipt);
54969
+ return receipt;
54970
+ }
54971
+ function privatePublicationResult(directory, receipt) {
54972
+ 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.";
54974
+ return {
54975
+ recoveryDirectory: directory,
54976
+ skillId: receipt.skillId,
54977
+ intentId: receipt.intent?.id ?? null,
54978
+ state,
54979
+ versionId: receipt.intent?.versionId ?? null,
54980
+ committed,
54981
+ executionEnabled: false,
54982
+ nextAction
54983
+ };
54984
+ }
54985
+ async function continuePrivatePublication(client, directory, options) {
54986
+ if (options.confirm !== true)
54987
+ throw new PrivatePublicationError("PUBLICATION_CONFIRM_REQUIRED", "Explicit upload confirmation is required.");
54988
+ if (options.waitMs !== undefined && (!Number.isSafeInteger(options.waitMs) || options.waitMs < 0 || options.waitMs > 300000))
54989
+ return fail();
54990
+ return locked(directory, () => continueLocked(client, directory, options));
54991
+ }
54992
+ async function continueLocked(client, directory, options) {
54993
+ const { receipt, bytes } = readPrivatePublicationRecovery(directory);
54994
+ bind(client, receipt);
54995
+ if (!receipt.intent) {
54996
+ receipt.phase = "begin_uncertain";
54997
+ save(directory, receipt);
54998
+ receipt.intent = await client.begin(receipt.skillId, receipt.declaration);
54999
+ receipt.phase = "awaiting_upload";
55000
+ save(directory, receipt);
55001
+ } else {
55002
+ receipt.intent = checkedPublicationView(await client.get(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
55003
+ save(directory, receipt);
55004
+ }
55005
+ if (receipt.intent.state === "awaiting_upload") {
55006
+ if (receipt.phase === "awaiting_upload") {
55007
+ receipt.phase = "upload_uncertain";
55008
+ save(directory, receipt);
55009
+ try {
55010
+ await client.upload(receipt.skillId, receipt.intent, bytes);
55011
+ } catch (error) {
55012
+ if (error instanceof PrivatePublicationError && !error.uncertain) {
55013
+ receipt.phase = "awaiting_upload";
55014
+ save(directory, receipt);
55015
+ }
55016
+ throw error;
55017
+ }
55018
+ receipt.phase = "uploaded";
55019
+ save(directory, receipt);
55020
+ }
55021
+ receipt.phase = "finalize_uncertain";
55022
+ save(directory, receipt);
55023
+ receipt.intent = checkedPublicationView(await client.finalize(receipt.skillId, receipt.intent.id), receipt.skillId, receipt.intent.id, receipt.declaration);
55024
+ receipt.phase = "observed";
55025
+ save(directory, receipt);
55026
+ }
55027
+ if (options.waitMs !== undefined && options.waitMs > 0 && ["queued", "verifying"].includes(receipt.intent.state)) {
55028
+ receipt.intent = checkedPublicationView(await client.wait(receipt.skillId, receipt.intent.id, { timeoutMs: options.waitMs }), receipt.skillId, receipt.intent.id, receipt.declaration);
55029
+ receipt.phase = "observed";
55030
+ save(directory, receipt);
55031
+ }
55032
+ return privatePublicationResult(directory, receipt);
55033
+ }
55034
+ async function inspectPrivatePublication(client, directory, cancel = false) {
55035
+ return locked(directory, () => inspectLocked(client, directory, cancel));
55036
+ }
55037
+ async function inspectLocked(client, directory, cancel) {
55038
+ const { receipt } = readPrivatePublicationRecovery(directory);
55039
+ bind(client, receipt);
55040
+ if (receipt.intent) {
55041
+ receipt.intent = checkedPublicationView(await (cancel ? client.cancel(receipt.skillId, receipt.intent.id) : client.get(receipt.skillId, receipt.intent.id)), receipt.skillId, receipt.intent.id, receipt.declaration);
55042
+ save(directory, receipt);
55043
+ } else if (cancel)
55044
+ throw new PrivatePublicationError("PUBLICATION_INTENT_UNKNOWN", "Reconcile the saved begin request with publication resume before cancelling its intent.");
55045
+ return privatePublicationResult(directory, receipt);
55046
+ }
54281
55047
  export {
54282
55048
  verifyContentHashFromEntries,
54283
55049
  validateRunLifecycleEvent,
@@ -54307,7 +55073,9 @@ export {
54307
55073
  registerRoutes,
54308
55074
  redactRunOutput,
54309
55075
  receiptId,
55076
+ readPrivatePublicationRecovery,
54310
55077
  protocolStateOf,
55078
+ preparePrivatePublication,
54311
55079
  packSkillBundle,
54312
55080
  noticeLocalSkillsMode,
54313
55081
  normalizeSkillsApiOrigin,
@@ -54321,6 +55089,7 @@ export {
54321
55089
  isSkillsLocalOptIn,
54322
55090
  isActiveStatus,
54323
55091
  inspectSkillBundle,
55092
+ inspectPrivatePublication,
54324
55093
  getServerSkillMd,
54325
55094
  getServerSkill,
54326
55095
  expiresAtFor,
@@ -54345,6 +55114,7 @@ export {
54345
55114
  createGovernanceStore,
54346
55115
  createCancelService,
54347
55116
  createAwsEcsClient,
55117
+ continuePrivatePublication,
54348
55118
  configuredSkillsApiUrl,
54349
55119
  computeContentHashFromEntries,
54350
55120
  clientTokenFor,
@@ -54381,6 +55151,8 @@ export {
54381
55151
  RemoteSkillsAuthClient,
54382
55152
  RemoteRouteUnsupportedError,
54383
55153
  RemoteRequestError,
55154
+ RemoteQuoteUnavailableError,
55155
+ RemotePrivatePublicationsClient,
54384
55156
  RemoteInvitationEmailUnconfirmedError,
54385
55157
  RemoteInvitationEmailError,
54386
55158
  RemoteCreditApprovalError,
@@ -54390,8 +55162,10 @@ export {
54390
55162
  RUN_LIFECYCLE_EVENT_TYPES,
54391
55163
  RUN_LIFECYCLE_EVENT_FIELDS,
54392
55164
  REMOTE_SKILL_RUN_CONTRACT_VERSION,
55165
+ PrivatePublicationError,
54393
55166
  PostgresSkillsStore,
54394
55167
  PostgresGovernanceStore,
55168
+ PRIVATE_PUBLICATION_MAX_BYTES,
54395
55169
  MissingSkillsFleetError,
54396
55170
  MemorySkillsStore,
54397
55171
  MemoryRunExecutionStore,