@hasna/skills 0.10.76 → 0.10.77

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
@@ -80,7 +80,11 @@ Stations provisioned with a fleet key keep using the internal gateway
80
80
  no recorded URL (the Keychain item, `HASNA_SKILLS_API_KEY` alone, or an older
81
81
  credentials file) always stays on that gateway and is never sent anywhere else.
82
82
  The gateway has no interactive login service, so on such a station
83
- `skills login` says so, sends nothing, and names the credential to unset first.
83
+ `skills login` says so and sends nothing. A stored gateway credential or vault
84
+ reference is preserved even with `--url`. Use a separate credential profile for
85
+ customer sign-in: `skills --profile skillsmd login`. Later product commands use that
86
+ profile too, for example `skills --profile skillsmd whoami --json`; fleet
87
+ commands keep their original configuration.
84
88
  Obtain a workspace API key through your administrator's provisioning process
85
89
  and configure it using the [credential resolution](#credentials) below. Check
86
90
  the selected identity and available capabilities before syncing:
@@ -3318,6 +3322,6 @@ membership too; an executable digest does not prove dynamic backend behavior or
3318
3322
  native adoption. These captures read files and never launch a native consumer.
3319
3323
 
3320
3324
 
3321
- Codex cache witnesses migrate separately through `skills hook rebind-cache --agent codex`. Preview requires the exact current policy and configuration SHA-256 values. Name each reviewed remote app-only parent with `--app-only-parent`; name each already reviewed native `mcp_tool` parent with `--reviewed-hook-parent`. Hook parents require exact enrolled manifest and hook bytes. When the owning installer created a remote-install receipt, its exact schema, identity and bytes are required. A local marketplace install with no receipt is accepted only when the exact full marketplace source is already enrolled and the unchanged Codex settings enable that exact plugin identity. The complete cache census refuses unknown parents, cache-member aliases, new skills, direct hooks selecting cache inputs, and changed app/MCP/hook/receipt or marketplace capabilities. Reviewed whole Codex root aliases keep their existing identity/CAS guards; hook selection through either the alias or canonical cache path is refused. Commands containing the direct `plugins/cache` selector are conservatively refused, including HOME, tilde and relative spellings; no shell evaluation is performed. App-only bootstrap verifies the old full manifest and receipt sources, then explicitly reviews current app capabilities; it does not claim those app bytes were historically witnessed.
3325
+ Codex cache witnesses migrate separately through `skills hook rebind-cache --agent codex`. Preview requires the exact current policy and configuration SHA-256 values. Name each reviewed remote app-only parent with `--app-only-parent`; name each already reviewed native `mcp_tool` parent with `--reviewed-hook-parent`. Hook parents require exact enrolled manifest and hook bytes. When the owning installer created a remote-install receipt, its exact schema, identity and bytes are required. A local marketplace install with no receipt is accepted only when the exact full marketplace source is already enrolled and the unchanged Codex settings enable that exact plugin identity. The complete cache census refuses unknown parents, cache-member aliases, new skills, direct hooks selecting cache inputs, and changed app/MCP/hook/receipt or marketplace capabilities. It also refuses every capability location Codex reads in a plugin version root that the witness does not bind (the Agent Plugins root `plugin.json`, `.claude-plugin` and `.cursor-plugin` manifests, a root `hooks.json`, `commands/` and `agents/` directories, and a skill document not spelled exactly `SKILL.md`), compared without regard to case; the vendor-refresh guard reads the same list. A semantic binding captured by an earlier release is checked against this census at every guard run, so one of these paths makes it refuse until a fresh discovery review. Reviewed whole Codex root aliases keep their existing identity/CAS guards; hook selection through either the alias or canonical cache path is refused. Commands containing the direct `plugins/cache` selector are conservatively refused, including HOME, tilde and relative spellings; no shell evaluation is performed. App-only bootstrap verifies the old full manifest and receipt sources, then explicitly reviews current app capabilities; it does not claim those app bytes were historically witnessed.
3322
3326
 
3323
3327
  Review the returned semantic cache digest, then repeat with `--expected-cache-witness-sha256 <digest> --apply`. The owning apply path preserves/readbacks the original policy, recomputes the whole plan, compares policy/config/cache preimages, writes only the Skills policy, and verifies the resulting consumer. Historical directory hashes and source rows remain retained as provenance; this command cannot reconstruct an unavailable old directory tree or waive an ancestor directory containing unrelated inputs. Equivalent version materializations and inert assets then leave the new Codex-specific witness unchanged. Every other discovery source, directory, settings witness, native control and session pin stays under its existing contract. Settings and cache upgrades remain independent; neither silently repairs drift in the other.
package/bin/index.js CHANGED
@@ -36888,7 +36888,7 @@ var package_default;
36888
36888
  var init_package = __esm(() => {
36889
36889
  package_default = {
36890
36890
  name: "@hasna/skills",
36891
- version: "0.10.76",
36891
+ version: "0.10.77",
36892
36892
  description: "Skills library for AI coding agents",
36893
36893
  type: "module",
36894
36894
  bin: {
@@ -50455,6 +50455,40 @@ var init_codex_settings_witness_v1 = __esm(() => {
50455
50455
  VERBOSITY = new Set(["low", "medium", "high"]);
50456
50456
  });
50457
50457
 
50458
+ // src/lib/codex-plugin-capability-paths.ts
50459
+ function foldCodexPluginEntryName(name) {
50460
+ return name.toUpperCase().toLowerCase();
50461
+ }
50462
+ function codexPluginRootCapabilityEntry(name) {
50463
+ const folded = foldCodexPluginEntryName(name);
50464
+ return CODEX_PLUGIN_ROOT_CAPABILITY_ENTRIES.includes(folded) ? folded : null;
50465
+ }
50466
+ function isCodexSkillDocumentName(name) {
50467
+ return foldCodexPluginEntryName(name) === "skill.md";
50468
+ }
50469
+ var CODEX_PLUGIN_MANIFEST_LOCATIONS, CODEX_PLUGIN_MANIFEST_DIRECTORIES, CODEX_PLUGIN_ROOT_CAPABILITY_ENTRIES;
50470
+ var init_codex_plugin_capability_paths = __esm(() => {
50471
+ CODEX_PLUGIN_MANIFEST_LOCATIONS = Object.freeze([
50472
+ "plugin.json",
50473
+ ".codex-plugin/plugin.json",
50474
+ ".claude-plugin/plugin.json",
50475
+ ".cursor-plugin/plugin.json"
50476
+ ]);
50477
+ CODEX_PLUGIN_MANIFEST_DIRECTORIES = Object.freeze([".codex-plugin", ".claude-plugin", ".cursor-plugin"]);
50478
+ CODEX_PLUGIN_ROOT_CAPABILITY_ENTRIES = Object.freeze([
50479
+ "plugin.json",
50480
+ ".codex-plugin",
50481
+ ".claude-plugin",
50482
+ ".cursor-plugin",
50483
+ "hooks",
50484
+ "hooks.json",
50485
+ ".mcp.json",
50486
+ ".app.json",
50487
+ "commands",
50488
+ "agents"
50489
+ ]);
50490
+ });
50491
+
50458
50492
  // src/lib/codex-hook-mcp-dependencies.ts
50459
50493
  import { createHash as createHash13 } from "crypto";
50460
50494
  import { basename as basename6, dirname as dirname11, isAbsolute as isAbsolute11, join as join17, resolve as resolve12, sep as sep4 } from "path";
@@ -50599,15 +50633,13 @@ function captureCodexHookMcpDependencies(configPath, reviewed, configText, chang
50599
50633
  }
50600
50634
  };
50601
50635
  const root = join17(cacheRoot, identity3, selectCodexPluginVersion(versions, selectionBudget));
50602
- const agent = read2(join17(root, "plugin.json"));
50636
+ const [agentPluginManifest, ...legacyManifests] = CODEX_PLUGIN_MANIFEST_LOCATIONS;
50637
+ const agent = read2(join17(root, agentPluginManifest));
50603
50638
  if (agent !== null) {
50604
50639
  const schema = JSON.parse(agent).$schema;
50605
50640
  need5(typeof schema !== "string" || !schema.startsWith("https://agent-plugins.org/schemas/"));
50606
50641
  }
50607
- const codex = read2(join17(root, ".codex-plugin/plugin.json"));
50608
- const claude = read2(join17(root, ".claude-plugin/plugin.json"));
50609
- const cursor = read2(join17(root, ".cursor-plugin/plugin.json"));
50610
- const text2 = codex ?? claude ?? cursor;
50642
+ const text2 = legacyManifests.map((location) => read2(join17(root, location))).find((value) => value !== null) ?? null;
50611
50643
  if (text2 === null)
50612
50644
  continue;
50613
50645
  const manifest = JSON.parse(text2);
@@ -50629,6 +50661,7 @@ var init_codex_hook_mcp_dependencies = __esm(() => {
50629
50661
  init_claude_settings_witness();
50630
50662
  init_agent_discovery_directories();
50631
50663
  init_agent_discovery_bytes();
50664
+ init_codex_plugin_capability_paths();
50632
50665
  events = new Set(["PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart", "SubagentStop", "Stop", "Interrupt"]);
50633
50666
  });
50634
50667
 
@@ -52942,15 +52975,17 @@ function codexVendorConfigCandidates(text2, cache3, options = {}) {
52942
52975
  var CODEX_VENDOR_MARKETPLACES, CODEX_REMOTE_VENDOR_MARKETPLACES, identifier3 = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value), record4 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value)), directory = (path) => {
52943
52976
  const stat2 = lstatSync14(path, { throwIfNoEntry: false });
52944
52977
  return Boolean(stat2?.isDirectory() && !stat2.isSymbolicLink());
52945
- }, OTHER_PLUGIN_CONTROLS, VENDOR_MANIFEST_KEYS, DEFAULT_SKILLS_PATHS, VENDOR_APP_DECLARATION = "./.app.json", MAX_SUBTREE_ENTRIES = 20000, MAX_SUBTREE_DEPTH = 32, hiddenName = (name) => name.startsWith(".") || name === "node_modules", isSkillDocument = (name) => name.toLowerCase() === "skill.md", present = (path) => lstatSync14(path, { throwIfNoEntry: false }) !== undefined, holdsOtherControl = (path) => readdirSync7(path).some((name) => OTHER_PLUGIN_CONTROLS.includes(name.toLowerCase())), MAX_TRAILING_VENDOR_ROWS = 16, MAX_VENDOR_TABLES = 8, MAX_CONFIG_CANDIDATES = 128, HEADER;
52978
+ }, OTHER_PLUGIN_CONTROLS, VENDOR_MANIFEST_KEYS, DEFAULT_SKILLS_PATHS, VENDOR_APP_DECLARATION = "./.app.json", MAX_SUBTREE_ENTRIES = 20000, MAX_SUBTREE_DEPTH = 32, hiddenName = (name) => name.startsWith(".") || name === "node_modules", isSkillDocument, present = (path) => lstatSync14(path, { throwIfNoEntry: false }) !== undefined, holdsOtherControl = (path) => readdirSync7(path).some((name) => OTHER_PLUGIN_CONTROLS.includes(codexPluginRootCapabilityEntry(name) ?? "")), MAX_TRAILING_VENDOR_ROWS = 16, MAX_VENDOR_TABLES = 8, MAX_CONFIG_CANDIDATES = 128, HEADER;
52946
52979
  var init_codex_vendor_refresh = __esm(() => {
52947
52980
  init_claude_settings_witness();
52948
52981
  init_codex_plugin_skill_controls();
52982
+ init_codex_plugin_capability_paths();
52949
52983
  CODEX_VENDOR_MARKETPLACES = Object.freeze(["openai-bundled", "openai-curated", "openai-curated-remote"]);
52950
52984
  CODEX_REMOTE_VENDOR_MARKETPLACES = Object.freeze(["openai-curated-remote"]);
52951
- OTHER_PLUGIN_CONTROLS = Object.freeze(["plugin.json", ".claude-plugin", ".cursor-plugin", "hooks", "hooks.json", ".mcp.json", "commands", "agents"]);
52985
+ OTHER_PLUGIN_CONTROLS = Object.freeze(CODEX_PLUGIN_ROOT_CAPABILITY_ENTRIES.filter((entry) => entry !== ".codex-plugin" && entry !== ".app.json"));
52952
52986
  VENDOR_MANIFEST_KEYS = Object.freeze(["name", "version", "description", "author", "homepage", "repository", "license", "keywords", "interface", "skills", "apps"]);
52953
52987
  DEFAULT_SKILLS_PATHS = Object.freeze(["./skills", "./skills/"]);
52988
+ isSkillDocument = isCodexSkillDocumentName;
52954
52989
  HEADER = /^[ \t]*(\[\[|\[)[ \t]*([^[\]\r\n]+?)[ \t]*(\]\]|\])[ \t]*(?:#[^\r\n]*)?\r?\n?$/;
52955
52990
  });
52956
52991
 
@@ -54056,6 +54091,10 @@ function skillsFileHasCredential(file) {
54056
54091
  return false;
54057
54092
  return /^\s*(?:export\s+)?(?:HASNA_SKILLS_API_KEY|SKILLS_API_KEY|HASNA_SKILLS_API_KEY_REF)\s*=\s*\S/m.test(text2);
54058
54093
  }
54094
+ function skillsFileHasVaultReference(file) {
54095
+ const text2 = readMetadataText(file);
54096
+ return text2 !== null && /^\s*(?:export\s+)?HASNA_SKILLS_API_KEY_REF\s*=\s*\S/m.test(text2);
54097
+ }
54059
54098
  var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
54060
54099
  var init_instance_credentials = __esm(() => {
54061
54100
  init_transport();
@@ -54090,6 +54129,25 @@ var init_local_opt_in = __esm(() => {
54090
54129
  // src/lib/product-default.ts
54091
54130
  var SKILLS_PRODUCT_DEFAULT_ORIGIN = "https://skills.md";
54092
54131
 
54132
+ // src/lib/credential-profile-command.ts
54133
+ function skillsShellArgument(value) {
54134
+ return /^[A-Za-z0-9._:/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
54135
+ }
54136
+ function skillsApiUrlEnvironment(origin) {
54137
+ return urlKeys.map((name) => `${name}=${skillsShellArgument(origin)}`).join(" ");
54138
+ }
54139
+ function skillsProfileCommand(command, env3 = process.env, origin) {
54140
+ const profile = selectedSkillsProfile(env3);
54141
+ const prefix = origin && urlKeys.some((name) => env3[name]?.trim()) ? `${skillsApiUrlEnvironment(origin)} ` : "";
54142
+ return `${prefix}skills${profile ? ` --profile ${profile}` : ""} ${command}`;
54143
+ }
54144
+ var urlKeys;
54145
+ var init_credential_profile_command = __esm(() => {
54146
+ init_instance_credentials();
54147
+ init_transport();
54148
+ urlKeys = clientTransportEnvKeys("skills").apiUrlKeys;
54149
+ });
54150
+
54093
54151
  // src/lib/fleet-credentials.ts
54094
54152
  var exports_fleet_credentials = {};
54095
54153
  __export(exports_fleet_credentials, {
@@ -54114,6 +54172,8 @@ __export(exports_fleet_credentials, {
54114
54172
  gatewaySignInRefusal: () => gatewaySignInRefusal,
54115
54173
  credentialPairing: () => credentialPairing,
54116
54174
  configuredSkillsApiUrl: () => configuredSkillsApiUrl,
54175
+ captureSkillsSignInTarget: () => captureSkillsSignInTarget,
54176
+ assertSkillsSignInCredentialReplaceable: () => assertSkillsSignInCredentialReplaceable,
54117
54177
  SkillsFleetCredentialError: () => SkillsFleetCredentialError,
54118
54178
  SKILLS_LOCAL_OPT_IN_ENV_KEYS: () => SKILLS_LOCAL_OPT_IN_ENV_KEYS,
54119
54179
  SKILLS_CREDENTIAL_TIERS: () => SKILLS_CREDENTIAL_TIERS,
@@ -54419,8 +54479,24 @@ function gatewaySignInRefusal(target) {
54419
54479
  if (normalizeSkillsApiOrigin(target.origin) !== normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
54420
54480
  return null;
54421
54481
  const cause = target.credentialSource ? `${target.credentialSource} holds a Skills credential for the internal gateway` : target.source === "--url" ? "--url names the internal gateway" : `${target.source} selects the internal gateway`;
54422
- const unset2 = target.credentialSource ?? (target.source === "--url" ? null : target.source);
54423
- return new SkillsFleetCredentialError(`${cause}, which has no sign-in service. Nothing was sent.` + (unset2 ? ` To sign in to the default server instead, first unset ${unset2} (or remove it from where it is stored), then run: skills login.` : ""), "GATEWAY_AUTH_UNAVAILABLE", unset2 ? [`unset ${unset2}`, "skills login"] : undefined);
54482
+ const command = (SKILLS_API_URL_ENV_KEYS.includes(target.source) ? `${skillsApiUrlEnvironment(SKILLS_PRODUCT_DEFAULT_ORIGIN)} ` : "") + "skills --profile <new-profile> login";
54483
+ return new SkillsFleetCredentialError(`${cause}, which has no sign-in service. Nothing was sent. ` + `Keep the fleet configuration and use a separate profile for product sign-in: ${command}.`, "GATEWAY_AUTH_UNAVAILABLE", [command]);
54484
+ }
54485
+ function assertSkillsSignInCredentialReplaceable(env3 = process.env, profile) {
54486
+ const files = skillsProfileCredentialFiles(env3, profile);
54487
+ const verifyUnchanged = captureSkillsCredentialFiles(files);
54488
+ const gateway = normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP));
54489
+ const command = (SKILLS_API_URL_ENV_KEYS.some((name) => env3[name]?.trim()) ? `${skillsApiUrlEnvironment(SKILLS_PRODUCT_DEFAULT_ORIGIN)} ` : "") + "skills --profile <new-profile> login";
54490
+ for (const file of files) {
54491
+ if (!skillsFileHasCredential(file))
54492
+ continue;
54493
+ const metadata = readSkillsInstanceMetadata(file);
54494
+ const instance = metadata.binding ?? metadata.apiUrl;
54495
+ if (skillsFileHasVaultReference(file) || !instance || normalizeSkillsApiOrigin(instance) === gateway) {
54496
+ throw new SkillsFleetCredentialError("The selected profile contains a protected fleet credential or vault reference. Nothing was sent or changed. " + `Use a separate profile for product sign-in: ${command}.`, "STORED_CREDENTIAL_PROTECTED", [command]);
54497
+ }
54498
+ }
54499
+ verifyUnchanged();
54424
54500
  }
54425
54501
  function explicitSignInOrigin(url) {
54426
54502
  if (!url.trim() || /[\x00-\x1f\x7f]/.test(url)) {
@@ -54448,13 +54524,15 @@ function resolveSkillsSignInOrigin(env3 = process.env, options = {}, explicitUrl
54448
54524
  const snapshot = snapshotSkillsEnvironment(env3);
54449
54525
  const resolvedOptions = snapshotSkillsOptions(env3, options);
54450
54526
  const profile = resolvedOptions.credentials?.profile;
54527
+ assertSkillsSignInCredentialReplaceable(snapshot, profile);
54451
54528
  const configured = configuredSkillsApiUrl(snapshot, resolvedOptions.credentials?.keychain, profile);
54452
54529
  const declared = configured && configured.source !== "default" ? configured : null;
54453
54530
  if (explicitUrl !== undefined) {
54454
54531
  const origin = explicitSignInOrigin(explicitUrl);
54455
54532
  const storedHere = declared ? skillsProfileCredentialFiles(snapshot, profile).includes(declared.source) : false;
54456
54533
  if (declared && !storedHere && normalizeSkillsApiOrigin(declared.value) !== origin) {
54457
- throw new SkillsFleetCredentialError(`--url ${origin} conflicts with ${declared.source}, which outranks the saved configuration and would ` + `shadow the key this sign-in stores. Unset ${declared.source} or pass the same URL.`, "INVALID_API_URL");
54534
+ const command = `${skillsApiUrlEnvironment(origin)} skills --profile <new-profile> login`;
54535
+ throw new SkillsFleetCredentialError(`--url ${origin} conflicts with ${declared.source}, which outranks the saved configuration and would ` + `shadow the key this sign-in stores. Keep the existing configuration and use a separate process and profile: ${command}.`, "INVALID_API_URL", [command]);
54458
54536
  }
54459
54537
  return { origin, source: "--url" };
54460
54538
  }
@@ -54486,6 +54564,14 @@ function resolveSkillsSignInOrigin(env3 = process.env, options = {}, explicitUrl
54486
54564
  throw error;
54487
54565
  }
54488
54566
  }
54567
+ function captureSkillsSignInTarget(env3 = process.env, options = {}, explicitUrl) {
54568
+ const snapshot = snapshotSkillsEnvironment(env3);
54569
+ const profile = options.credentials?.profile ?? selectedCliSkillsCredentialProfile();
54570
+ const unchanged2 = captureSkillsCredentialFiles(skillsProfileCredentialFiles(snapshot, profile));
54571
+ const target = resolveSkillsSignInOrigin(env3, options, explicitUrl);
54572
+ unchanged2();
54573
+ return { ...target, unchanged: unchanged2 };
54574
+ }
54489
54575
  function requireSkillsFleet(action = "This command", env3 = process.env, options = {}) {
54490
54576
  const fleet = resolveSkillsFleet(env3, options);
54491
54577
  if (fleet.mode === "hosted")
@@ -54497,6 +54583,7 @@ var init_fleet_credentials = __esm(() => {
54497
54583
  init_transport();
54498
54584
  init_instance_credentials();
54499
54585
  init_local_opt_in();
54586
+ init_credential_profile_command();
54500
54587
  init_local_opt_in();
54501
54588
  ENV_KEYS = clientTransportEnvKeys(SKILLS_APP);
54502
54589
  SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
@@ -56074,14 +56161,23 @@ function assertParents(cacheRoot, controls, appOnlyParents, hookParents, files,
56074
56161
  continue;
56075
56162
  const parent = join28(cacheRoot, parts[0], parts[1]);
56076
56163
  const root = join28(parent, parts[2]);
56077
- const base2 = parts[parts.length - 1];
56078
- if (parts.includes(".codex-plugin") && (parts.length < 4 || parts[3] !== ".codex-plugin" || (parts.length === 4 ? base2 !== ".codex-plugin" : parts.length !== 5 || base2 !== "plugin.json")))
56164
+ const base2 = parts[parts.length - 1], folded = foldCodexPluginEntryName(base2);
56165
+ if (parts.length === 3 && (codexPluginRootCapabilityEntry(base2) !== null || isCodexSkillDocumentName(base2)))
56166
+ refuse4();
56167
+ const rootEntry = parts.length >= 4 ? codexPluginRootCapabilityEntry(parts[3]) : null;
56168
+ if (rootEntry !== null && rootEntry !== parts[3])
56169
+ refuse4();
56170
+ if (rootEntry !== null && !SEMANTIC_BOUND_ROOT_ENTRIES.includes(rootEntry))
56079
56171
  refuse4();
56080
- if ([".app.json", ".mcp.json"].includes(base2) && (parts.length !== 4 || dirname18(member) !== root))
56172
+ if (isCodexSkillDocumentName(base2) && base2 !== "SKILL.md")
56081
56173
  refuse4();
56082
- if ([".app.json", ".mcp.json"].includes(base2) && hookParents.some((reviewed) => reviewed.pluginParent === parent))
56174
+ if (parts.some((part) => CODEX_PLUGIN_MANIFEST_DIRECTORIES.includes(foldCodexPluginEntryName(part))) && (parts.length < 4 || parts[3] !== ".codex-plugin" || (parts.length === 4 ? base2 !== ".codex-plugin" : parts.length !== 5 || base2 !== "plugin.json")))
56083
56175
  refuse4();
56084
- if ((parts[3] === "hooks" || base2 === "hooks.json") && (![4, 5].includes(parts.length) || parts[3] !== "hooks" || (parts.length === 4 ? base2 !== "hooks" : base2 !== "hooks.json") || !hookParents.some((reviewed) => reviewed.pluginParent === join28(cacheRoot, parts[0], parts[1]))))
56176
+ if ([".app.json", ".mcp.json"].includes(folded) && (parts.length !== 4 || dirname18(member) !== root))
56177
+ refuse4();
56178
+ if ([".app.json", ".mcp.json"].includes(folded) && hookParents.some((reviewed) => reviewed.pluginParent === parent))
56179
+ refuse4();
56180
+ if ((rootEntry === "hooks" || folded === "hooks.json") && (![4, 5].includes(parts.length) || parts[3] !== "hooks" || (parts.length === 4 ? base2 !== "hooks" : base2 !== "hooks.json") || !hookParents.some((reviewed) => reviewed.pluginParent === join28(cacheRoot, parts[0], parts[1]))))
56085
56181
  refuse4();
56086
56182
  }
56087
56183
  }
@@ -56303,13 +56399,15 @@ function assertCodexSemanticCacheWitnessUnchanged(previous, current) {
56303
56399
  if (hash2(priorProjection) !== previous.sha256 || hash2(currentProjection) !== current.sha256 || canonical8(comparable(priorProjection)) !== canonical8(comparable(currentProjection)))
56304
56400
  refuse4();
56305
56401
  }
56306
- var CODEX_SEMANTIC_CACHE_WITNESS_SCHEMA = "hasna.codex-semantic-cache-witness.v1", identifier5 = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value), digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value), refuse4 = () => {
56402
+ var CODEX_SEMANTIC_CACHE_WITNESS_SCHEMA = "hasna.codex-semantic-cache-witness.v1", SEMANTIC_BOUND_ROOT_ENTRIES, identifier5 = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value), digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value), refuse4 = () => {
56307
56403
  throw new Error("CODEX_SEMANTIC_CACHE_WITNESS_UNSUPPORTED: review Codex plugin cache identity and controls");
56308
56404
  }, MAX_DOCUMENT_BYTES, MAX_RECEIPT_BYTES, codexHookEvents;
56309
56405
  var init_codex_semantic_cache_witness = __esm(() => {
56310
56406
  init_claude_settings_witness();
56311
56407
  init_codex_plugin_skill_controls();
56312
56408
  init_codex_native_skill_catalog();
56409
+ init_codex_plugin_capability_paths();
56410
+ SEMANTIC_BOUND_ROOT_ENTRIES = Object.freeze([".codex-plugin", ".app.json", ".mcp.json", "hooks"]);
56313
56411
  MAX_DOCUMENT_BYTES = 1024 * 1024;
56314
56412
  MAX_RECEIPT_BYTES = 16 * 1024;
56315
56413
  codexHookEvents = new Set([
@@ -66250,7 +66348,8 @@ function getAuthConfig(env3 = process.env, options = {}) {
66250
66348
  return null;
66251
66349
  return { apiKey: fleet.apiKey, ...readIdentity(env3) };
66252
66350
  }
66253
- function writeCredentialValues(values2, env3 = process.env) {
66351
+ function writeCredentialValues(values2, env3 = process.env, beforeWrite) {
66352
+ beforeWrite?.();
66254
66353
  const file = skillsCredentialFilePath(env3);
66255
66354
  mkdirSync14(dirname29(file), { recursive: true, mode: 448 });
66256
66355
  const lines = [];
@@ -66279,9 +66378,16 @@ function writeCredentialValues(values2, env3 = process.env) {
66279
66378
  `) + `
66280
66379
  `;
66281
66380
  const temp = `${file}.tmp-${process.pid}`;
66282
- writeFileSync11(temp, body, { mode: 384 });
66283
- chmodSync2(temp, 384);
66284
- renameSync8(temp, file);
66381
+ writeFileSync11(temp, body, { mode: 384, flag: "wx" });
66382
+ try {
66383
+ chmodSync2(temp, 384);
66384
+ beforeWrite?.();
66385
+ renameSync8(temp, file);
66386
+ } finally {
66387
+ try {
66388
+ unlinkSync6(temp);
66389
+ } catch {}
66390
+ }
66285
66391
  return file;
66286
66392
  }
66287
66393
  function readCredentialValue(key2, env3 = process.env) {
@@ -66325,7 +66431,14 @@ function safeOrigin(url) {
66325
66431
  return null;
66326
66432
  }
66327
66433
  }
66328
- function saveAuthConfig(config, env3 = process.env, authenticatedOrigin, issuedBy) {
66434
+ function saveAuthConfig(config, env3 = process.env, authenticatedOrigin, issuedBy, assertUnchanged) {
66435
+ const unchanged3 = assertUnchanged ?? captureSkillsCredentialFiles(skillsProfileCredentialFiles(env3));
66436
+ const beforeWrite = () => {
66437
+ unchanged3();
66438
+ if (issuedBy)
66439
+ assertSkillsSignInCredentialReplaceable(env3);
66440
+ };
66441
+ beforeWrite();
66329
66442
  const apiKey = config.apiKey.trim();
66330
66443
  if (!apiKey)
66331
66444
  throw new Error("Refusing to store an empty Skills API key.");
@@ -66337,7 +66450,7 @@ function saveAuthConfig(config, env3 = process.env, authenticatedOrigin, issuedB
66337
66450
  const previous = readIdentityRecord(env3);
66338
66451
  const loginWroteBefore = previous?.urlWrittenBy === "login" && typeof previous.apiUrl === "string" && safeOrigin(previous.apiUrl) === apiUrl;
66339
66452
  const urlWrittenByLogin = issuedBy !== undefined && (!previousUrl || safeOrigin(previousUrl) !== apiUrl || loginWroteBefore);
66340
- const file = writeCredentialValues({ SKILLS_API_KEY: null, SKILLS_API_URL: null, [SKILLS_API_KEY_REF]: null, [SKILLS_API_KEY_ENV]: apiKey, [SKILLS_BOUND_API_URL]: apiUrl, [SKILLS_API_URL_ENV]: apiUrl }, env3);
66453
+ const file = writeCredentialValues({ SKILLS_API_KEY: null, SKILLS_API_URL: null, [SKILLS_API_KEY_REF]: null, [SKILLS_API_KEY_ENV]: apiKey, [SKILLS_BOUND_API_URL]: apiUrl, [SKILLS_API_URL_ENV]: apiUrl }, env3, beforeWrite);
66341
66454
  const identity6 = {};
66342
66455
  for (const field3 of ["email", "orgId", "orgSlug", "userId"]) {
66343
66456
  const value = config[field3];
@@ -88497,7 +88610,10 @@ function clearPendingDeviceSignIn(env3 = process.env, origin) {
88497
88610
  }
88498
88611
  }
88499
88612
  }
88500
- async function persistSignIn(result2, origin, env3 = process.env, issuedBy = "sign-in") {
88613
+ async function persistSignIn(result2, origin, env3 = process.env, issuedBy = "sign-in", assertUnchanged) {
88614
+ const unchanged3 = assertUnchanged ?? captureSkillsCredentialFiles(skillsProfileCredentialFiles(env3));
88615
+ unchanged3();
88616
+ assertSkillsSignInCredentialReplaceable(env3);
88501
88617
  let apiKey = typeof result2.apiKey === "string" && result2.apiKey.trim() ? result2.apiKey : undefined;
88502
88618
  if (!apiKey && typeof result2.token === "string" && result2.token) {
88503
88619
  const created = await new RemoteSkillsAuthClient(origin).request("/api/auth/keys", {
@@ -88515,7 +88631,7 @@ async function persistSignIn(result2, origin, env3 = process.env, issuedBy = "si
88515
88631
  ...boundedText(result2.organization?.id) ? { orgId: result2.organization.id } : {},
88516
88632
  ...boundedText(result2.organization?.slug) ? { orgSlug: result2.organization.slug } : {},
88517
88633
  ...boundedText(result2.user?.id) ? { userId: result2.user.id } : {}
88518
- }, env3, origin, issuedBy);
88634
+ }, env3, origin, issuedBy, unchanged3);
88519
88635
  }
88520
88636
  function manualRevocationHint(origin) {
88521
88637
  if (origin === SKILLS_PRODUCT_DEFAULT_ORIGIN) {
@@ -111493,6 +111609,7 @@ async function prepareWorkspaceEnrollment(membershipId, source = process.env) {
111493
111609
  const env3 = { ...source }, profile = selectedSkillsProfile(env3);
111494
111610
  if (!profile)
111495
111611
  return fail4("Workspace login requires an explicit HASNA_PROFILE name.");
111612
+ assertSkillsSignInCredentialReplaceable(env3);
111496
111613
  if (["HASNA_SKILLS_API_KEY_OVERRIDE", "HASNA_SKILLS_API_KEY_REF", "HASNA_SKILLS_API_KEY", "SKILLS_API_KEY"].some((name) => env3[name]?.trim()))
111497
111614
  return fail4("Clear injected API keys before enrolling a named workspace profile.");
111498
111615
  const origin = getApiUrl("Sign in to a workspace", env3);
@@ -124513,6 +124630,14 @@ async function codeFor(client2, options) {
124513
124630
  return promptCode();
124514
124631
  }
124515
124632
  function errorResult(error61, json3) {
124633
+ if (error61 instanceof SkillsFleetCredentialError) {
124634
+ if (json3)
124635
+ console.log(JSON.stringify({ error: error61.message, code: error61.code, next: error61.next }));
124636
+ else
124637
+ console.error(error61.message);
124638
+ process.exitCode = 1;
124639
+ return;
124640
+ }
124516
124641
  const message = error61 instanceof NameInputError || error61 instanceof WorkspaceProfileError ? error61.message : "Unable to complete workspace sign-in. Check the selected server, profile, account and fresh verification code.";
124517
124642
  if (json3)
124518
124643
  console.log(JSON.stringify({ error: message }));
@@ -124566,6 +124691,7 @@ One workspace key saved. Use HASNA_PROFILE=${result2.profile} for subsequent com
124566
124691
  }
124567
124692
  var init_workspace_selection = __esm(() => {
124568
124693
  init_auth_store();
124694
+ init_fleet_credentials();
124569
124695
  init_remote_auth();
124570
124696
  init_workspace_profile();
124571
124697
  init_customer_verification();
@@ -124711,7 +124837,7 @@ function printWhoami(payload) {
124711
124837
  }
124712
124838
  function signInTarget(url2, json3) {
124713
124839
  try {
124714
- const target = resolveSkillsSignInOrigin(process.env, {}, url2);
124840
+ const target = captureSkillsSignInTarget(process.env, {}, url2);
124715
124841
  const refusal3 = gatewaySignInRefusal(target);
124716
124842
  if (refusal3)
124717
124843
  throw refusal3;
@@ -124722,7 +124848,10 @@ function signInTarget(url2, json3) {
124722
124848
  }
124723
124849
  }
124724
124850
  function targetFlag(target) {
124725
- return target.source === "--url" ? ` --url ${target.origin}` : "";
124851
+ return target.source === "--url" ? ` --url ${skillsShellArgument(target.origin)}` : "";
124852
+ }
124853
+ function skillsCommand(command, target) {
124854
+ return skillsProfileCommand(command, process.env, target.origin);
124726
124855
  }
124727
124856
  async function finishSignIn(result2, target, json3) {
124728
124857
  const email5 = result2.user?.email;
@@ -124734,7 +124863,7 @@ async function finishSignIn(result2, target, json3) {
124734
124863
  ...organization2 ? { organization: organization2 } : {},
124735
124864
  ...result2.firstLogin !== undefined ? { firstLogin: result2.firstLogin } : {},
124736
124865
  apiUrl: target.origin,
124737
- next: ["skills setup agents"]
124866
+ next: [skillsCommand("setup agents", target)]
124738
124867
  }));
124739
124868
  return;
124740
124869
  }
@@ -124744,20 +124873,20 @@ async function finishSignIn(result2, target, json3) {
124744
124873
  console.log(source_default.dim(` Organization: ${result2.organization.name}`));
124745
124874
  console.log(source_default.dim(` Server: ${target.origin}`));
124746
124875
  console.log(source_default.dim(` API key saved to ${getAuthFilePath()}`));
124747
- await offerAgentSetup();
124876
+ await offerAgentSetup(target);
124748
124877
  }
124749
- async function offerAgentSetup() {
124878
+ async function offerAgentSetup(target) {
124750
124879
  const before2 = process.exitCode;
124751
124880
  const answer = await prompt(`
124752
- Register Skills with your coding agents now (skills setup agents)? [Y/n] `);
124881
+ Register Skills with your coding agents now (${skillsCommand("setup agents", target)})? [Y/n] `);
124753
124882
  if (answer === null) {
124754
124883
  process.exitCode = before2 ?? 0;
124755
124884
  console.log(source_default.dim(`
124756
- Skipped. Run it any time: skills setup agents`));
124885
+ Skipped. Run it any time: ${skillsCommand("setup agents", target)}`));
124757
124886
  return;
124758
124887
  }
124759
124888
  if (answer !== "" && !/^y(es)?$/i.test(answer)) {
124760
- console.log(source_default.dim("Skipped. Run it any time: skills setup agents"));
124889
+ console.log(source_default.dim(`Skipped. Run it any time: ${skillsCommand("setup agents", target)}`));
124761
124890
  return;
124762
124891
  }
124763
124892
  const { handleMcp: handleMcp2 } = await Promise.resolve().then(() => (init_runtime_mcp(), exports_runtime_mcp));
@@ -124834,7 +124963,7 @@ async function doLogin(email5, code2, json3, target) {
124834
124963
  if (!json3)
124835
124964
  console.log(source_default.green("\u2713 Code sent to " + email5));
124836
124965
  if (json3 || !isTTY || !process.stderr.isTTY) {
124837
- console.log(JSON.stringify({ status: "code_sent", email: email5, message: `Supply the fresh six-digit code on stdin to: skills login --email ${email5} --code-stdin${targetFlag(target)}` }));
124966
+ console.log(JSON.stringify({ status: "code_sent", email: email5, message: `Supply the fresh six-digit code on stdin to: ${skillsCommand("login", target)} --email ${skillsShellArgument(email5)} --code-stdin${targetFlag(target)}` }));
124838
124967
  return;
124839
124968
  }
124840
124969
  const answer = await promptCode();
@@ -124844,6 +124973,7 @@ async function doLogin(email5, code2, json3, target) {
124844
124973
  }
124845
124974
  let verifyRes;
124846
124975
  try {
124976
+ target.unchanged?.();
124847
124977
  verifyRes = await apiRequest("/api/auth/verify", {
124848
124978
  method: "POST",
124849
124979
  body: JSON.stringify({ email: email5, code: code2 })
@@ -124857,7 +124987,7 @@ async function doLogin(email5, code2, json3, target) {
124857
124987
  return;
124858
124988
  }
124859
124989
  try {
124860
- await persistSignIn(verifyRes, origin, env3, "sign-in");
124990
+ await persistSignIn(verifyRes, origin, env3, "sign-in", target.unchanged);
124861
124991
  } catch (err) {
124862
124992
  writeCommandError(err, "Login succeeded but API key creation failed", json3);
124863
124993
  return;
@@ -124886,6 +125016,7 @@ async function doApiKeyLogin(apiKey, json3, target) {
124886
125016
  }
124887
125017
  let whoami;
124888
125018
  try {
125019
+ target.unchanged?.();
124889
125020
  whoami = await apiRequest("/api/auth/whoami", {
124890
125021
  headers: { Authorization: `Bearer ${trimmed}` }
124891
125022
  }, origin);
@@ -124898,20 +125029,25 @@ async function doApiKeyLogin(apiKey, json3, target) {
124898
125029
  const orgId = stringField2(identity6.orgId);
124899
125030
  const orgSlug = stringField2(identity6.organization);
124900
125031
  const userId = stringField2(identity6.userId);
124901
- saveAuthConfig({
124902
- apiKey: trimmed,
124903
- ...email5 ? { email: email5 } : {},
124904
- ...orgId ? { orgId } : {},
124905
- ...orgSlug ? { orgSlug } : {},
124906
- ...userId ? { userId } : {}
124907
- }, env3, origin, "api-key");
125032
+ try {
125033
+ saveAuthConfig({
125034
+ apiKey: trimmed,
125035
+ ...email5 ? { email: email5 } : {},
125036
+ ...orgId ? { orgId } : {},
125037
+ ...orgSlug ? { orgSlug } : {},
125038
+ ...userId ? { userId } : {}
125039
+ }, env3, origin, "api-key", target.unchanged);
125040
+ } catch (error61) {
125041
+ writeCommandError(error61, "API key verified but the selected profile changed", json3);
125042
+ return;
125043
+ }
124908
125044
  if (json3 || !isTTY) {
124909
- console.log(JSON.stringify({ ...identity6, status: "authenticated", apiUrl: origin, next: ["skills setup agents"] }, null, 2));
125045
+ console.log(JSON.stringify({ ...identity6, status: "authenticated", apiUrl: origin, next: [skillsCommand("setup agents", target)] }, null, 2));
124910
125046
  return;
124911
125047
  }
124912
125048
  printWhoami(identity6);
124913
125049
  console.log(source_default.dim(`Server: ${origin}`));
124914
- await offerAgentSetup();
125050
+ await offerAgentSetup(target);
124915
125051
  }
124916
125052
  function printDeviceCode(session, resumed, write) {
124917
125053
  write(source_default.bold(resumed ? `
@@ -124932,12 +125068,12 @@ async function doDeviceLogin(options, target) {
124932
125068
  const origin = target.origin;
124933
125069
  const json3 = Boolean(options.json);
124934
125070
  const interactive = Boolean(isTTY && !json3);
124935
- const next = `skills login --poll${targetFlag(target)}`;
125071
+ const next = `${skillsCommand("login", target)} --poll${targetFlag(target)}`;
124936
125072
  let session = options.poll ? loadPendingDeviceSignIn(origin, env3) : null;
124937
125073
  if (options.poll && !session) {
124938
125074
  const others = pendingDeviceSignInOrigins(env3).filter((other) => other !== origin);
124939
125075
  if (others.length > 0) {
124940
- writeCommandError(new Error(`No pending sign-in for ${origin}. Pending: ${others.map((other) => `skills login --poll --url ${other}`).join("; ")}`), "No pending sign-in", json3);
125076
+ writeCommandError(new Error(`No pending sign-in for ${origin}. Pending: ${others.map((other) => `${skillsProfileCommand("login", process.env, other)} --poll --url ${skillsShellArgument(other)}`).join("; ")}`), "No pending sign-in", json3);
124941
125077
  return;
124942
125078
  }
124943
125079
  }
@@ -124988,7 +125124,10 @@ Waiting for authentication...`));
124988
125124
  try {
124989
125125
  const deviceCode = session.deviceCode;
124990
125126
  outcome = await pollDeviceAuthorization({
124991
- poll: () => pollDeviceToken(origin, deviceCode),
125127
+ poll: () => {
125128
+ target.unchanged?.();
125129
+ return pollDeviceToken(origin, deviceCode);
125130
+ },
124992
125131
  intervalSeconds: session.interval,
124993
125132
  deadline: Math.min(Date.now() + timeoutMs, session.expiresAt),
124994
125133
  onSlowDown: (intervalMs) => {
@@ -125002,8 +125141,8 @@ Waiting for authentication...`));
125002
125141
  }
125003
125142
  if (outcome.status !== "authorized") {
125004
125143
  const ended = {
125005
- expired: "The sign-in code expired before it was approved. Start again: skills login",
125006
- invalid: "The server no longer recognises this sign-in code. Start again: skills login",
125144
+ expired: `The sign-in code expired before it was approved. Start again: ${skillsCommand("login", target)}${targetFlag(target)}`,
125145
+ invalid: `The server no longer recognises this sign-in code. Start again: ${skillsCommand("login", target)}${targetFlag(target)}`,
125007
125146
  denied: "Sign-in was denied in the browser.",
125008
125147
  timeout: `Device login timed out before browser authentication completed. Resume with: ${next}`,
125009
125148
  cancelled: "Sign-in cancelled."
@@ -125019,7 +125158,7 @@ Waiting for authentication...`));
125019
125158
  return;
125020
125159
  }
125021
125160
  try {
125022
- await persistSignIn(outcome.result, origin, env3, "sign-in");
125161
+ await persistSignIn(outcome.result, origin, env3, "sign-in", target.unchanged);
125023
125162
  } catch (err) {
125024
125163
  writeCommandError(err, "Login succeeded but API key creation failed", json3);
125025
125164
  return;
@@ -125271,6 +125410,7 @@ var init_auth = __esm(() => {
125271
125410
  init_auth_store();
125272
125411
  init_fleet_credentials();
125273
125412
  init_remote_client();
125413
+ init_credential_profile_command();
125274
125414
  init_sign_in();
125275
125415
  init_remote_auth();
125276
125416
  isTTY = process.stdin.isTTY && process.stdout.isTTY;
@@ -133962,6 +134102,7 @@ function InstallProgress({
133962
134102
  // src/cli/components/AccountView.tsx
133963
134103
  var import_react29 = __toESM(require_react(), 1);
133964
134104
  init_fleet_credentials();
134105
+ init_credential_profile_command();
133965
134106
  init_sign_in();
133966
134107
  var jsx_dev_runtime6 = __toESM(require_jsx_dev_runtime(), 1);
133967
134108
  var SLASH_COMMANDS = [
@@ -134021,7 +134162,7 @@ function AccountView({ action, argument, onDone }) {
134021
134162
  });
134022
134163
  return;
134023
134164
  }
134024
- const target = resolveSkillsSignInOrigin(process.env, {}, argument);
134165
+ const target = captureSkillsSignInTarget(process.env, {}, argument);
134025
134166
  const refusal3 = gatewaySignInRefusal(target);
134026
134167
  if (refusal3)
134027
134168
  throw refusal3;
@@ -134032,13 +134173,16 @@ function AccountView({ action, argument, onDone }) {
134032
134173
  update2({ kind: "waiting", userCode: start.userCode, url: url2, origin: target.origin });
134033
134174
  openVerificationPage(url2, target.origin);
134034
134175
  const outcome = await pollDeviceAuthorization({
134035
- poll: () => pollDeviceToken(target.origin, start.deviceCode),
134176
+ poll: () => {
134177
+ target.unchanged?.();
134178
+ return pollDeviceToken(target.origin, start.deviceCode);
134179
+ },
134036
134180
  intervalSeconds: start.interval,
134037
134181
  deadline: Date.now() + start.expiresIn * 1000,
134038
134182
  signal: controller.signal
134039
134183
  });
134040
134184
  if (outcome.status === "authorized") {
134041
- await persistSignIn(outcome.result, target.origin);
134185
+ await persistSignIn(outcome.result, target.origin, process.env, "sign-in", target.unchanged);
134042
134186
  const email4 = outcome.result.user?.email;
134043
134187
  update2({
134044
134188
  kind: "done",
@@ -134046,16 +134190,17 @@ function AccountView({ action, argument, onDone }) {
134046
134190
  lines: [
134047
134191
  { text: email4 ? `Signed in as ${email4}` : "Signed in", tone: "good" },
134048
134192
  { text: `Server: ${target.origin}`, tone: "dim" },
134049
- { text: "Register Skills with your agents from a shell: skills setup agents", tone: "dim" }
134193
+ { text: `Register Skills with your agents from a shell: ${skillsProfileCommand("setup agents", process.env, target.origin)}`, tone: "dim" }
134050
134194
  ]
134051
134195
  });
134052
134196
  return;
134053
134197
  }
134198
+ const retry = argument !== undefined ? `/login ${target.origin}` : "/login";
134054
134199
  const ended = {
134055
- expired: "The sign-in code expired before it was approved. Type /login to start again.",
134056
- invalid: "The server no longer recognises this sign-in code. Type /login to start again.",
134200
+ expired: `The sign-in code expired before it was approved. Type ${retry} to start again.`,
134201
+ invalid: `The server no longer recognises this sign-in code. Type ${retry} to start again.`,
134057
134202
  denied: "Sign-in was denied in the browser.",
134058
- timeout: "Sign-in timed out. Type /login to start again.",
134203
+ timeout: `Sign-in timed out. Type ${retry} to start again.`,
134059
134204
  cancelled: "Sign-in cancelled."
134060
134205
  }[outcome.status];
134061
134206
  update2({ kind: "done", changed: false, lines: [{ text: ended, tone: "bad" }] });