@hasna/skills 0.10.71 → 0.10.73

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.
@@ -1,3 +1,4 @@
1
+ import { type NativeDiscoveryWitnessTolerance } from "./native-discovery-witness-change.js";
1
2
  /** Recursive path/type membership only; source-file hashes bind reviewed bytes. */
2
3
  export interface DiscoveryDirectory {
3
4
  path: string;
@@ -10,8 +11,12 @@ export interface DiscoveryDirectoryProjection extends DiscoveryDirectory {
10
11
  * reviewed inactive roots. It never replaces a historical full witness. */
11
12
  export declare function captureDiscoveryDirectoryProjection(paths: string[], omittedRoots: readonly string[]): DiscoveryDirectoryProjection[];
12
13
  /** Compare a planner-captured inventory after removing only exact inert graphs.
13
- * The stored rows must reproduce their original digest before projection. */
14
- export declare function verifyDiscoveryDirectoryProjection(directories: DiscoveryDirectoryProjection[], omittedRoots: readonly string[]): void;
14
+ * The stored rows must reproduce their original digest before projection. A
15
+ * membership change is a NativeDiscoveryWitnessChange: thrown, unless the live
16
+ * Codex prompt hook's tolerance accepts it. Invalid inventories always throw. */
17
+ export declare function verifyDiscoveryDirectoryProjection(directories: DiscoveryDirectoryProjection[], omittedRoots: readonly string[], tolerance?: NativeDiscoveryWitnessTolerance): void;
15
18
  /** Read-only capture; callers must review both membership and source bytes. */
16
19
  export declare function captureDiscoveryDirectories(paths: string[], excludedRoots?: readonly string[]): DiscoveryDirectory[];
17
- export declare function verifyDiscoveryDirectories(directories: DiscoveryDirectory[], excludedRoots?: readonly string[]): void;
20
+ /** A membership change is a NativeDiscoveryWitnessChange: thrown, unless the
21
+ * live Codex prompt hook's tolerance accepts it. Invalid collections always throw. */
22
+ export declare function verifyDiscoveryDirectories(directories: DiscoveryDirectory[], excludedRoots?: readonly string[], tolerance?: NativeDiscoveryWitnessTolerance): void;
@@ -1,6 +1,8 @@
1
1
  import type { IntegrationAgent } from "./agent-adapters.js";
2
2
  import { type DiscoveryDirectory } from "./agent-discovery-directories.js";
3
3
  import { type CodexReviewedHookSource } from "./codex-hook-mcp-dependencies.js";
4
+ import { CLAUDE_ORPHAN_MARKER_MODE } from "./claude-plugin-cache-marker.js";
5
+ import { type NativeDiscoveryWitnessTolerance } from "./native-discovery-witness-change.js";
4
6
  import { type ManagedPluginRegistrationWitness } from "./plugin-discovery.js";
5
7
  import { type CodexHookDiscoveryRecovery } from "./codex-hook-discovery-recovery.js";
6
8
  import { type CodexPluginSkillControl, type CodexPluginSourceInput } from "./codex-plugin-skill-controls.js";
@@ -11,7 +13,7 @@ export interface DiscoverySource {
11
13
  reviewArtifact?: RetainedReviewArtifact;
12
14
  path: string;
13
15
  sha256: string | null;
14
- hashMode?: "bytes" | "path-bytes" | "claude-plugin-manifest-v1" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-settings-v4" | "claude-marketplace-registry-v2" | "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "codex-settings-v5" | "codex-settings-v6" | "executable-v1" | "sumi-settings-v1" | "claude-marketplace-entry-v1";
16
+ hashMode?: "bytes" | "path-bytes" | "claude-plugin-orphaned-at-v1" | "claude-plugin-manifest-v1" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-settings-v4" | "claude-marketplace-registry-v2" | "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "codex-settings-v5" | "codex-settings-v6" | "executable-v1" | "sumi-settings-v1" | "claude-marketplace-entry-v1";
15
17
  hookSources?: CodexReviewedHookSource[];
16
18
  managedPlugins?: ManagedPluginRegistrationWitness[];
17
19
  format?: "json" | "toml" | "yaml";
@@ -94,23 +96,56 @@ export declare function projectNativeDiscoveryFields(text: string, format: "json
94
96
  /** Called only after the owning planner validated discovery and capabilities.
95
97
  * New roots must already be body-free; absent roots need the prior typed proof. */
96
98
  export declare function captureRetiredCodexDiscovery(binding: AgentDiscoveryBinding, controls: CodexPluginSkillControl[], rules: unknown): AgentDiscoveryBinding;
99
+ /** The witness class that may keep its reviewed digest beneath a positively
100
+ * verified absent root: a full byte witness, the installer's full manifest
101
+ * projection or a present version-root orphan marker. Null for every other
102
+ * source, which must stay current. */
103
+ export type RetainedAbsentWitness = "bytes" | "claude-plugin-manifest-v1" | typeof CLAUDE_ORPHAN_MARKER_MODE;
97
104
  /** Only native capability locations covered by the complete typed cache census
98
105
  * qualify. Historical sources remain stored; arbitrary missing files do not. */
99
106
  export declare function codexSemanticCacheSourceRole(source: DiscoverySource, cacheRoot: string, parents: readonly string[], hookParents?: readonly string[]): boolean;
100
107
  export declare function codexSemanticCacheDirectoryRole(path: string, cacheRoot: string): boolean;
101
108
  export declare function assertCodexSemanticCacheHookSafety(cacheRoot: string, parents: readonly string[], configPath: string, hookRootAlias?: CodexSemanticCacheDiscovery["hookRootAlias"]): void;
109
+ /** Source-byte and directory-membership drift are NativeDiscoveryWitnessChange
110
+ * errors. Only the live Codex prompt hook passes `witnessTolerance`; a change it
111
+ * accepts as a vendor refresh is collected and the remaining checks still run.
112
+ * Every other change and every other failure throws as before. */
102
113
  export declare function verifyAgentDiscovery(binding: AgentDiscoveryBinding, codexRecovery?: CodexHookDiscoveryRecovery, reviewedRootAliases?: readonly {
103
114
  agent: IntegrationAgent;
104
115
  alias: string;
105
116
  target: string;
106
- }[]): void;
117
+ }[], witnessTolerance?: NativeDiscoveryWitnessTolerance): void;
118
+ /** One stored Claude discovery source as an installer sees it. */
119
+ export type ClaudeDiscoverySourceVerdict = {
120
+ status: "current";
121
+ } | {
122
+ status: "retained-absent";
123
+ root: string;
124
+ witness: RetainedAbsentWitness;
125
+ } | {
126
+ status: "drift";
127
+ reason: "MISSING" | "APPEARED" | "CHANGED" | "MARKER_INVALID" | "REFUSED";
128
+ };
129
+ /** Read-only, per-source report on a stored Claude discovery binding, in
130
+ * stored order. A source is retained only through the same absent retired
131
+ * version rule and predicate verifyAgentDiscovery applies; a refused rule
132
+ * retains nothing. Every other source is checked by its own witness, exactly
133
+ * as verification checks it (a typed orphan marker by the marker predicate).
134
+ * This report never accepts a binding by itself: a caller must also require
135
+ * verifyAgentDiscovery to accept the same binding. */
136
+ export declare function classifyClaudeDiscoverySources(binding: AgentDiscoveryBinding): ClaudeDiscoverySourceVerdict[];
107
137
  export declare function rebindAgentDiscovery(binding: AgentDiscoveryBinding, changes: Map<string, string>): AgentDiscoveryBinding;
108
138
  /** Explicit streaming integrity witness. Never read an executable as text or execute it. */
109
139
  export declare function captureDiscoveryExecutableSources(paths: string[]): DiscoverySource[];
110
- /** Capture full byte witnesses for reviewed source or executable files; never import or execute them. */
140
+ /** Capture exact source bytes, or typed Claude cache marker presence; never execute them. */
111
141
  export declare function captureDiscoveryByteSources(paths: string[]): DiscoverySource[];
112
142
  /** Capture executable/source path identity, including links and absent targets. */
113
143
  export declare function captureDiscoveryPathSources(paths: string[]): DiscoverySource[];
144
+ /** Top-level keys whose presence, at any value, makes a project Claude settings
145
+ * file a higher-precedence discovery source that needs review. Claude 2.1.300
146
+ * rewrites `additionalMarketplaces` into `extraKnownMarketplaces` on every
147
+ * settings read, including local settings, so the alias counts as well. */
148
+ export declare const CLAUDE_PROJECT_DISCOVERY_KEYS: readonly ["enabledPlugins", "extraKnownMarketplaces", "additionalMarketplaces", "skillOverrides"];
114
149
  /** Project settings can introduce a higher-precedence discovery source. Until
115
150
  * its native merge format is supported, refuse that layer rather than certify
116
151
  * the home-only inventory. Ordinary unrelated project settings remain usable. */
@@ -120,13 +155,30 @@ export declare function assertProjectDiscovery(agent: IntegrationAgent, director
120
155
  export declare function agentDiscoveryConfigPath(home: string, agent: IntegrationAgent): string;
121
156
  /** Re-derive an automatic binding and compare it with the stored one. A
122
157
  * mismatch is drift, except when the only difference is a runtime this process
123
- * cannot resolve: then it is DISCOVERY_ROOT_UNRESOLVED, which stays blocking. */
158
+ * cannot resolve: then it is DISCOVERY_ROOT_UNRESOLVED, which stays blocking.
159
+ * Only the live Codex prompt hook passes `witnessTolerance`: when roots, method
160
+ * and every source and directory path are unchanged, only witness digests differ
161
+ * and the tolerance accepts every one of them, they are collected instead of
162
+ * refusing. */
124
163
  export declare function verifyAutomaticDiscoveryClosure(binding: AgentDiscoveryBinding, options: {
125
164
  home: string;
126
165
  canonical: (path: string) => string;
127
166
  change: string;
128
167
  executable?: DiscoveryExecutable;
168
+ witnessTolerance?: NativeDiscoveryWitnessTolerance;
129
169
  }): void;
170
+ /** True only when the changed Codex config.toml differs from its reviewed
171
+ * witness by appended vendor disable rows and/or vendor plugin disables:
172
+ * removing those entries or restoring a formerly enabled vendor flag reproduces
173
+ * the stored digest. Witness modes
174
+ * without a replacement digest (V6, artifacts, managed registries) stay strict. */
175
+ export declare function codexVendorConfigChangeOnly(source: DiscoverySource, cache: string): boolean;
176
+ /** Whether replacing `path` with `text` leaves every recorded witness of it
177
+ * satisfied, judged by each source's own digest mode without touching the file,
178
+ * with the same screen-reader receipt tolerance typed V1-V5 verification applies.
179
+ * Fails closed: a review artifact, managed-plugin or hook-source witness, a
180
+ * semantic cache binding of the file, or an unsupported mode counts as changed. */
181
+ export declare function discoveryTextKeepsWitness(binding: Pick<AgentDiscoveryBinding, "sources" | "codexSemanticCache">, path: string, text: string): boolean;
130
182
  /** `executable` is the agent's runtime: omitted, it is resolved through PATH;
131
183
  * a value (or null for none) is used as given, so verification never depends
132
184
  * on the caller's PATH. */
@@ -2,6 +2,7 @@ import { type CodexCorpusWriteOptions } from "./codex-corpus-write.js";
2
2
  import { type ReviewedCodexSkillDenial } from "./codex-plugin-skill-controls.js";
3
3
  import { type CodexNativeHookEnvelope, type ProcessInspector, type NativePolicyHelperRunner } from "./codex-native-skill-policy.js";
4
4
  import { type CodexNativeSkillCatalog } from "./codex-native-skill-catalog.js";
5
+ import type { NativeDiscoveryWitnessKind } from "./native-discovery-witness-change.js";
5
6
  import { type DiscoveryExecutable, type AgentDiscoveryBinding, type ReviewedDiscoveryInputs } from "./agent-discovery.js";
6
7
  import { type IntegrationAgent } from "./agent-adapters.js";
7
8
  import type { CodexHookDiscoveryRecovery } from "./codex-hook-discovery-recovery.js";
@@ -279,15 +280,52 @@ interface ManagedBridgeOptions {
279
280
  profileId?: string;
280
281
  codexDiscoveryRecovery?: CodexHookDiscoveryRecovery;
281
282
  codexNativePolicy?: CodexNativePolicyGuardInput;
283
+ /** Live Codex prompt hook only. Exempt vendor Codex plugin-cache copies that
284
+ * Codex configuration provably disables, and tolerate, with a warning, the
285
+ * discovery-witness changes a vendor plugin refresh provably makes. Enabled
286
+ * vendor copies and every other change still refuse. Install, trust, migrate
287
+ * and sync never pass it. */
288
+ codexPromptHook?: boolean;
289
+ }
290
+ /** Result of a guard call; both lists are empty unless the caller opted in.
291
+ * `codexInertVendorCopies` holds the canonical skill directories exempted as
292
+ * disabled vendor copies; `codexDiscoveryChanges` the witnesses tolerated as a
293
+ * vendor plugin refresh. */
294
+ export interface ManagedBridgeReport {
295
+ codexInertVendorCopies: string[];
296
+ codexDiscoveryChanges: Array<{
297
+ kind: NativeDiscoveryWitnessKind;
298
+ path: string | null;
299
+ }>;
282
300
  }
283
301
  /** Package-classified installed-plugin content below the Codex plugin cache that
284
302
  * a verified restricted policy cannot load. Classification is the existing plugin
285
303
  * manifest identity and real-path binding, never a path prefix; user, repo and
286
304
  * project copies, unclassified cache files and any allowed host path refuse. */
287
305
  export declare function isInertCodexPluginCacheCopy(entry: NativeSkillEntry, cache: string, allowedHostPaths: readonly string[], read: (path: string) => string): boolean;
306
+ /** A vendor plugin-cache copy that Codex configuration provably disables, so its
307
+ * payload never loads natively. Vendor means: classified installed-plugin content
308
+ * in an allowlisted OpenAI marketplace (exact names), with the native remote
309
+ * install receipt for remote marketplaces, no Skills-managed or Hasna marker,
310
+ * and a plugin whose every cached version passes the fail-closed refresh check
311
+ * (codexVendorPluginRefreshSafe: one Codex manifest with only known keys, no
312
+ * hooks, MCP, command, agent or extension declaration, no other manifest
313
+ * location, no link and no skill hidden from the inventory or nested in
314
+ * another). Classification reads only the Codex manifest, so without that check
315
+ * a version could load payloads no rule here covers. Disabled means: a `[[skills.config]]`
316
+ * rule names its exact SKILL.md path or its plugin-qualified name with
317
+ * `enabled = false` (the native ordered evaluation agrees), and no matching rule
318
+ * enables it. A whole-plugin `[plugins."<id>"] enabled = false` table is never
319
+ * enough: remote state can enable a locally disabled remote plugin, and a
320
+ * cache copy carries no proof that its source is configuration-controlled. */
321
+ export declare function isInertCodexVendorPluginCopy(entry: NativeSkillEntry, cache: string, rules: unknown, read: (path: string) => string): boolean;
322
+ /** Visible, non-blocking warning for the discovery-witness changes the live Codex
323
+ * prompt hook tolerated as a vendor plugin refresh, or undefined when there were
324
+ * none. One bounded line; paths only, never payloads or file contents. */
325
+ export declare function codexPromptDriftWarning(report: ManagedBridgeReport): string | undefined;
288
326
  /** Run before any prompt context load. Missing ownership or reappearing native
289
327
  * discovery files require repair; verified cache availability is not an override. */
290
- export declare function assertManagedAgentBridge(agent: IntegrationAgent, options?: ManagedBridgeOptions): void;
328
+ export declare function assertManagedAgentBridge(agent: IntegrationAgent, options?: ManagedBridgeOptions): ManagedBridgeReport;
291
329
  export declare function normalizeAgentHookPrompt(agent: IntegrationAgent, event: string, prompt: string): string;
292
330
  export declare function hookContextOutput(event: string, result: {
293
331
  context: string;
@@ -0,0 +1,65 @@
1
+ import { type RetainedAbsentWitness } from "./agent-discovery.js";
2
+ export declare const CLAUDE_DISCOVERY_BINDING_VERIFICATION_SCHEMA: "skills.claude-discovery-binding-verification/v1";
3
+ /** The rule that lets a stored witness stay unverified because Claude removed
4
+ * its whole retired plugin-cache version while unchanged settings and
5
+ * registrations select a current sibling version. */
6
+ export declare const ABSENT_RETIRED_CLAUDE_VERSION_RULE: "absent-retired-claude-version";
7
+ export type ClaudeDiscoveryBindingRefusalCode = "POLICY_PATH_MISMATCH" | "POLICY_UNAVAILABLE" | "POLICY_UNREADABLE" | "BINDING_UNAVAILABLE" | "BINDING_INVALID" | "POLICY_CHANGED";
8
+ /** A request, policy or stored-binding read/parse failure: no verdict. */
9
+ export declare class ClaudeDiscoveryBindingRefusal extends Error {
10
+ readonly code: ClaudeDiscoveryBindingRefusalCode;
11
+ constructor(code: ClaudeDiscoveryBindingRefusalCode);
12
+ }
13
+ export type ClaudeBindingSourceFailure = "MISSING" | "APPEARED" | "CHANGED" | "MARKER_INVALID" | "REFUSED";
14
+ export type ClaudeBindingDirectoryFailure = "MEMBERSHIP_CHANGED" | "REFUSED";
15
+ export interface ClaudeBindingSourceReport {
16
+ index: number;
17
+ path: string;
18
+ hashMode: string | null;
19
+ format: string | null;
20
+ sha256: string | null;
21
+ status: "current" | "retained-absent" | "drift";
22
+ retention?: {
23
+ rule: typeof ABSENT_RETIRED_CLAUDE_VERSION_RULE;
24
+ root: string;
25
+ witness: RetainedAbsentWitness;
26
+ };
27
+ reason?: ClaudeBindingSourceFailure;
28
+ }
29
+ export interface ClaudeBindingDirectoryReport {
30
+ index: number;
31
+ path: string;
32
+ status: "current" | "drift";
33
+ reason?: ClaudeBindingDirectoryFailure;
34
+ }
35
+ export type ClaudeBindingFailure = {
36
+ kind: "source";
37
+ index: number;
38
+ path: string;
39
+ reason: ClaudeBindingSourceFailure;
40
+ } | {
41
+ kind: "directory";
42
+ index: number;
43
+ path: string;
44
+ reason: ClaudeBindingDirectoryFailure;
45
+ } | {
46
+ kind: "binding";
47
+ reason: "DISCOVERY_REFUSED";
48
+ };
49
+ export interface ClaudeDiscoveryBindingVerification {
50
+ schema: typeof CLAUDE_DISCOVERY_BINDING_VERIFICATION_SCHEMA;
51
+ ok: boolean;
52
+ policySha256: string;
53
+ discoverySha256: string;
54
+ agent: "claude";
55
+ method: "reviewed" | "automatic";
56
+ rootCount: number;
57
+ sources: ClaudeBindingSourceReport[];
58
+ directories: ClaudeBindingDirectoryReport[];
59
+ failures: ClaudeBindingFailure[];
60
+ }
61
+ /** Verify the stored `bridge.discovery.claude` binding of the package-owned
62
+ * Skills policy. `ok` is true only when the hook's own discovery verification
63
+ * accepts the binding and every source is current or retained under the
64
+ * absent retired version rule, and every directory witness is current. */
65
+ export declare function verifyClaudeInstallerDiscoveryBinding(policyPath: string): ClaudeDiscoveryBindingVerification;
@@ -0,0 +1,10 @@
1
+ import type { DiscoveryByteBudget } from "./agent-discovery-bytes.js";
2
+ export declare const CLAUDE_ORPHAN_MARKER_MODE: "claude-plugin-orphaned-at-v1";
3
+ /** True only for `<...>/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.orphaned_at`
4
+ * with exactly one cache segment in the path. Every other path, including a
5
+ * marker at another depth, keeps its ordinary exact witness. */
6
+ export declare function isClaudePluginCacheOrphanMarker(path: string): boolean;
7
+ /** Validate on every capture and check; only presence enters the digest.
8
+ * Claude rewrites the timestamp (and may replace the inode) between checks,
9
+ * so neither is bound, but the file may not change during one read. */
10
+ export declare function hashClaudePluginCacheOrphanMarker(path: string, budget?: DiscoveryByteBudget, changes?: Map<string, string>): string | null;
@@ -0,0 +1,11 @@
1
+ /** Whether Codex can load a skill below `path`, a `node_modules` directory
2
+ * `depth` levels below its plugin cache. A marketplace, plugin or version
3
+ * directory is an identity, so one named `node_modules` is always walked.
4
+ * Inside a version, the directory is walked when it lies in, or leads to, a
5
+ * manifest-declared skills path or the default `skills` root. Codex replaces
6
+ * the default with a declared path; Skills keeps walking it, as it walks every
7
+ * other visible directory in the cache. A manifest that cannot be read exactly
8
+ * (a link or special file, oversized, invalid UTF-8 or JSON, duplicate keys, an
9
+ * unsupported skills shape or an escaping path) walks it too, so an unreadable
10
+ * declaration never hides a loaded skill. */
11
+ export declare function codexPluginCacheSkillTree(path: string, depth: number): boolean;
@@ -32,6 +32,15 @@ export interface CodexPluginSourceInput {
32
32
  sourceSha256: string;
33
33
  }
34
34
  type Read = (path: string) => string;
35
+ /** The remote plugin identifier from the exact native install receipt Codex
36
+ * writes beside a remote plugin's versions, or null when it is absent or invalid. */
37
+ export declare function codexRemoteInstallReceipt(parent: string, read: Read): string | null;
38
+ /** Whether a vendor refresh's `.app.json` connector declaration is valid under
39
+ * the same appControls validation rootControls applies (duplicate keys, the
40
+ * single `apps` object, app names, `id`/`category`/`required` only, types and
41
+ * caps), read as a regular, unlinked file of at most 1 MiB in strict UTF-8.
42
+ * Every failure is false. */
43
+ export declare function codexAppDeclarationValid(path: string): boolean;
35
44
  export interface CodexPluginRootControls {
36
45
  namespace: string;
37
46
  manifestSha256: string;
@@ -1,5 +1,14 @@
1
1
  import { type ClaudeSettingsWitnessBudget } from "./claude-settings-witness.js";
2
2
  export declare function hashCodexSettingsReplacement(text: string, budget?: ClaudeSettingsWitnessBudget): string;
3
+ /** The parsed document and numeric spellings a V1 digest binds, before the
4
+ * documented inference fields are removed. Callers that edit the structure
5
+ * (codexSettingsToleratesScreenReaderReceipt) finish each edited copy separately. */
6
+ export declare function prepareCodexSettingsV1(text: string): {
7
+ config: Record<string, any>;
8
+ tokens: string[];
9
+ };
10
+ /** The V1 digest of a prepared structure. Mutates `config`. */
11
+ export declare function finishCodexSettingsV1(config: Record<string, any>, tokens: string[]): string;
3
12
  export declare function captureCodexSettings(path: string, budget?: ClaudeSettingsWitnessBudget): {
4
13
  path: string;
5
14
  hashMode: "codex-settings-v1";
@@ -5,6 +5,24 @@ import { type ClaudeSettingsWitnessBudget } from "./claude-settings-witness.js";
5
5
  * `codex-settings-v2` witness binds strictly more of the same file, so it
6
6
  * supersedes this projection instead of coexisting with it. */
7
7
  export declare const CODEX_DISCOVERY_PROJECTION_FIELDS: readonly ["plugins", "marketplaces", "skills"];
8
+ /** Typed modes whose digests bind the receipt. */
9
+ export type CodexReceiptSettingsMode = "codex-settings-v1" | "codex-settings-v2" | "codex-settings-v3" | "codex-settings-v4" | "codex-settings-v5";
10
+ /** How a current document relates to a typed V1-V5 witness's reviewed digest.
11
+ * `screen-reader-receipt` is the only tolerated difference. */
12
+ export type CodexSettingsWitnessMatch = "exact" | "screen-reader-receipt";
13
+ /** True when `text` differs from the document a typed V1-V5 witness reviewed
14
+ * only by the boolean screen-reader receipt, proven by reproducing `expected`
15
+ * under the witness's own unchanged digest from the current document with the
16
+ * receipt edited back. The text-bound projections (local MCP rows, V4 counts,
17
+ * numeric spelling) run once on the current text; a receipt line holds no
18
+ * numeric token. Digest values never change and nothing is recorded. Any other
19
+ * TUI key, a non-boolean value and every other difference stay bound.
20
+ * Fail-closed: errors return false. */
21
+ export declare function codexSettingsToleratesScreenReaderReceipt(text: string, hashMode: CodexReceiptSettingsMode, expected: string): boolean;
22
+ /** Read a typed V1-V5 settings witness file once and classify it against the
23
+ * reviewed digest: `exact`, `screen-reader-receipt` (the only tolerated
24
+ * difference), or null (drift). */
25
+ export declare function codexSettingsWitnessMatch(path: string, hashMode: CodexReceiptSettingsMode, expected: string, budget?: ClaudeSettingsWitnessBudget): CodexSettingsWitnessMatch | null;
8
26
  export declare function hashCodexSettingsReplacementV2(text: string, budget?: ClaudeSettingsWitnessBudget): string;
9
27
  export declare function captureCodexSettingsV2(path: string, budget?: ClaudeSettingsWitnessBudget): {
10
28
  path: string;
@@ -0,0 +1,54 @@
1
+ /** OpenAI's own Codex plugin marketplaces, by their exact native names in Codex
2
+ * 0.162.1. Only these hold vendor plugin copies; every other marketplace,
3
+ * including Hasna's and look-alike names, is never treated as vendor content. */
4
+ export declare const CODEX_VENDOR_MARKETPLACES: readonly string[];
5
+ /** Vendor marketplaces whose plugins Codex installs remotely and records with a
6
+ * `.codex-remote-plugin-install.json` receipt beside their versions. */
7
+ export declare const CODEX_REMOTE_VENDOR_MARKETPLACES: readonly string[];
8
+ /** The allowlisted vendor marketplace a canonical path lies in, at or below
9
+ * `<cache>/<marketplace>`, or null for the cache itself, other marketplaces and
10
+ * any non-canonical path. */
11
+ export declare function codexVendorMarketplace(cache: string, path: string): string | null;
12
+ /** A plugin version root a vendor refresh can add without review: its only
13
+ * manifest is a regular `.codex-plugin/plugin.json` (alone in its directory,
14
+ * no duplicate keys) whose keys are all in VENDOR_MANIFEST_KEYS, with the
15
+ * default skills root and, when present, exactly the `./.app.json` connector
16
+ * declaration backed by an `.app.json` that passes the strict connector
17
+ * validation rootControls applies (codexAppDeclarationValid); there is no hooks, MCP, command
18
+ * or agent location and no root, Claude or Cursor manifest; every skill in it
19
+ * is visible to the native inventory. Errors and bounds refuse. */
20
+ export declare function codexVendorPluginRootRefreshSafe(root: string, budget?: {
21
+ remaining: number;
22
+ }): boolean;
23
+ /** True only when every cached version of one vendor plugin
24
+ * (`<cache>/<marketplace>/<plugin>`) passes codexVendorPluginRootRefreshSafe,
25
+ * the plugin directory carries no manifest, hooks, MCP, command or agent
26
+ * location of its own, and no version is a link. Errors and bounds refuse. */
27
+ export declare function codexVendorPluginRefreshSafe(parent: string, budget?: {
28
+ remaining: number;
29
+ }): boolean;
30
+ /** True only when every plugin in a vendor marketplace subtree could have been
31
+ * added by a routine vendor refresh: each plugin passes
32
+ * codexVendorPluginRefreshSafe and the marketplace holds no link, special file
33
+ * or hidden directory. Plugin hooks and other skill roots need their own
34
+ * review, so a membership change that adds them is never a routine vendor
35
+ * refresh. The whole marketplace is checked, not only new versions, so anything
36
+ * unexpected anywhere refuses. Bounded; links are never followed; errors refuse. */
37
+ export declare function codexVendorSubtreeHookFree(cache: string, path: string): boolean;
38
+ /** Candidate earlier versions of a Codex config.toml that differ from `text`
39
+ * only by appended vendor disable rows and/or vendor plugin disables,
40
+ * the entries an operator or Codex adds when a vendor plugin refreshes. Each
41
+ * candidate is checked to parse to exactly `text` without those entries, so a
42
+ * candidate whose digest equals the reviewed witness proves the change is
43
+ * vendor-only. Anything the text model cannot remove exactly yields no
44
+ * candidate, which keeps the witness strict.
45
+ *
46
+ * Bounds: at most 8 vendor plugin tables, 16 trailing rows and 128 candidates,
47
+ * tried smallest edit first (fewest changed tables, then fewest removed rows).
48
+ * `rowsAffectDigest: false` (a typed V2-V5 witness, which ignores disable-only
49
+ * rows) skips row removal; with it, every change to at most two of eight
50
+ * tables fits the 128 candidates. Otherwise appended rows alone always fit.
51
+ * A bound only ever omits candidates, so it can only cause a refusal. */
52
+ export declare function codexVendorConfigCandidates(text: string, cache: string, options?: {
53
+ rowsAffectDigest?: boolean;
54
+ }): string[];
@@ -39,6 +39,8 @@ export interface SkillsConfig {
39
39
  defaultScope?: "global" | "project";
40
40
  format?: "compact" | "json" | "csv";
41
41
  extensionsDir?: string;
42
+ /** Managed hooks, global scope only: "auto" (default) migrates a refused expired session pin; "off" keeps the refusal. */
43
+ sessionPinMigration?: "auto" | "off";
42
44
  }
43
45
  export type ConfigScope = "global" | "project";
44
46
  /**
@@ -97,6 +99,24 @@ export declare function loadConfigReadOnly(): SkillsConfig;
97
99
  * Get the config file path for a given scope
98
100
  */
99
101
  export declare function getConfigPath(scope: ConfigScope): string;
102
+ /**
103
+ * Managed hooks migrate a session pin whose renewal was refused only because
104
+ * its profile moved on, unless the station turns that off with
105
+ * `skills config set sessionPinMigration off --global`. Only the global
106
+ * configuration is read: the prompt hook never reads a project-controlled file
107
+ * for this.
108
+ *
109
+ * This reads the file itself instead of readConfigFile, which returns {} for
110
+ * an unreadable or malformed file. An opt-out must survive a damaged file, so
111
+ * "auto" needs one of two confirmed states. Either lstat finds no entry at the
112
+ * path and its directory exists, or the entry is a regular file (not a
113
+ * symlink), opened without following links, holding a JSON object without the
114
+ * key or with exactly "auto". Everything else keeps the refusal: a dangling or
115
+ * other symlink, a directory or other non-regular entry, a missing or dangling
116
+ * data directory, any read error, a parse error (including a truncated write),
117
+ * a non-object document, a retired key, or a value other than "auto" or "off".
118
+ */
119
+ export declare function sessionPinMigrationEnabled(): boolean;
100
120
  /**
101
121
  * Load merged config: project-local overrides global
102
122
  */
@@ -3,7 +3,9 @@ export declare const HOOK_REFUSAL_REASON_CODES: readonly string[];
3
3
  export declare class HookDiagnosticError extends Error {
4
4
  readonly code: string;
5
5
  readonly stage: "context" | "sync";
6
- constructor(code: string, stage: "context" | "sync");
6
+ /** Fixed marker: a refused renewal whose automatic pin migration ran out of budget. */
7
+ readonly migration?: "timeout";
8
+ constructor(code: string, stage: "context" | "sync", migration?: "timeout");
7
9
  }
8
10
  export declare function hookChildError(stdout: string, stage: "context" | "sync"): HookDiagnosticError;
9
11
  export declare function hookFailureReason(error: unknown, profileId?: string): string;
@@ -0,0 +1,25 @@
1
+ import type { DiscoverySource } from "./agent-discovery.js";
2
+ /** What changed under a reviewed native discovery witness: a bound source
3
+ * file's projected bytes, or a bound directory's membership. */
4
+ export type NativeDiscoveryWitnessKind = "input" | "membership";
5
+ /** A reviewed discovery witness no longer matches the current file system. It is
6
+ * drift of something the review bound, never an invalid, missing or unresolvable
7
+ * binding, which keep their own plain errors. The message is unchanged from the
8
+ * plain error each site threw before, so every caller that does not tolerate
9
+ * these sees exactly the same refusal. */
10
+ export declare class NativeDiscoveryWitnessChange extends Error {
11
+ readonly kind: NativeDiscoveryWitnessKind;
12
+ /** Canonical path the witness bound, or null when the site names none. */
13
+ readonly path: string | null;
14
+ constructor(kind: NativeDiscoveryWitnessKind, path: string | null, message: string);
15
+ }
16
+ /** Live Codex prompt hook only. Decides, per changed witness, whether the change
17
+ * is provably one a Codex vendor plugin refresh makes; accepted changes are
18
+ * collected instead of refusing. Every other change still throws. */
19
+ export interface NativeDiscoveryWitnessTolerance {
20
+ acceptsInput(source: DiscoverySource): boolean;
21
+ acceptsMembership(path: string): boolean;
22
+ readonly changes: NativeDiscoveryWitnessChange[];
23
+ }
24
+ /** Throw the change unless an opted-in tolerance accepted it. */
25
+ export declare function witnessChanged(change: NativeDiscoveryWitnessChange, tolerance: NativeDiscoveryWitnessTolerance | undefined, accepted: boolean): void;
@@ -18,6 +18,11 @@ export interface SkillSessionParentBinding {
18
18
  generation: number;
19
19
  receiptSha256: string;
20
20
  }
21
+ /** The project lock a session's pin was inherited from: its absolute path and the SHA-256 of its exact bytes. */
22
+ export interface SkillSessionProjectLock {
23
+ path: string;
24
+ sha256: string;
25
+ }
21
26
  export interface SkillSessionReceipt extends CachedSelectionProfile {
22
27
  sessionId: string;
23
28
  loaded: string[];
@@ -25,6 +30,12 @@ export interface SkillSessionReceipt extends CachedSelectionProfile {
25
30
  generation?: number;
26
31
  /** Private local metadata binding a child receipt to the exact parent snapshot it inherited. */
27
32
  parent?: SkillSessionParentBinding;
33
+ /**
34
+ * Set when the session was created from a project lock (and inherited by its
35
+ * child sessions). Such a pin follows that intentional lock, so the managed
36
+ * hook never migrates it automatically, whatever the prompt's directory.
37
+ */
38
+ projectLock?: SkillSessionProjectLock;
28
39
  }
29
40
  export interface SkillSessionSnapshot extends SkillSessionParentBinding {
30
41
  path: string;
@@ -60,6 +71,11 @@ export declare function assertFreshCachedProfile(receipt: CachedSelectionProfile
60
71
  }): void;
61
72
  export declare function projectSelectionLockPath(projectDir: string): string;
62
73
  export declare function readProjectSelection(projectDir: string): CachedSelectionProfile | null;
74
+ /** The project lock with the provenance a session created from it records. */
75
+ export declare function readProjectSelectionSnapshot(projectDir: string): {
76
+ receipt: CachedSelectionProfile;
77
+ lock: SkillSessionProjectLock;
78
+ } | null;
63
79
  export declare function sessionReceiptPath(sessionId: string, options?: SelectionCacheOptions): string;
64
80
  export declare function readSkillSession(sessionId: string, options?: SelectionCacheOptions): SkillSessionReceipt | null;
65
81
  export declare function readSkillSessionSnapshotIfExists(sessionId: string, options?: SelectionCacheOptions): SkillSessionSnapshot | null;
@@ -77,6 +93,12 @@ export declare function inspectSessionWriteLock(sessionId: string, options?: Sel
77
93
  hostBound: boolean;
78
94
  };
79
95
  export declare function recoverSessionWriteLock(sessionId: string, reviewDigest: string, options?: SelectionCacheOptions): string;
96
+ /**
97
+ * Hold this session's receipt write lock for state that must change only with
98
+ * its pin (the managed-hook migration notice). Another holder refuses with
99
+ * SESSION_WRITE_BUSY; nothing waits.
100
+ */
101
+ export declare function withSkillSessionWriteLock<T>(sessionId: string, options: SelectionCacheOptions, action: (assertOwned: () => void) => T): T;
80
102
  export declare function nextSkillSessionGeneration(generation: number): number;
81
103
  /** Receipt creation and updates are exact-snapshot CAS operations. Child creation also fences its parent. */
82
104
  export declare function writeSkillSession(receipt: SkillSessionReceipt, expected: SkillSessionWritePrecondition, options?: SelectionCacheOptions): void;
@@ -2,7 +2,7 @@
2
2
  import { type ProfileClient } from "./profile-client.js";
3
3
  import type { ResolvedSkillProfile, ResolvedSkillSelection } from "../types/skill-selection.js";
4
4
  import type { SkillBundleEntry } from "./skill-bundle.js";
5
- import { type CachedSelectionProfile, type SelectionCacheOptions, type SkillSessionParentBinding, type SkillSessionReceipt } from "./selection-cache.js";
5
+ import { type CachedSelectionProfile, type SelectionCacheOptions, type SkillSessionParentBinding, type SkillSessionProjectLock, type SkillSessionReceipt } from "./selection-cache.js";
6
6
  export interface SelectionResolverOptions extends SelectionCacheOptions {
7
7
  client?: ProfileClient;
8
8
  cached?: boolean;
@@ -36,6 +36,8 @@ export interface ResolvedSelectionContext {
36
36
  parentSnapshot?: SkillSessionParentBinding;
37
37
  inheritedLoaded?: string[];
38
38
  client?: ProfileClient;
39
+ /** Set when a new session's pin comes from the project lock; the session receipt records it. */
40
+ projectLock?: SkillSessionProjectLock;
39
41
  }
40
42
  export declare function resolveSelectionContext(profileId: string, options?: SelectionResolverOptions): Promise<ResolvedSelectionContext>;
41
43
  export declare function readSelectedEntries(selection: ResolvedSkillSelection, context: ResolvedSelectionContext, options?: SelectionResolverOptions): Promise<SkillBundleEntry[]>;
@@ -1,5 +1,5 @@
1
1
  import { type ProfileClient } from "./profile-client.js";
2
- import { type SelectionCacheOptions } from "./selection-cache.js";
2
+ import { SkillSelectionError, type SelectionCacheOptions } from "./selection-cache.js";
3
3
  export interface SessionReconciliationInput {
4
4
  sessionId: string;
5
5
  fromProfile: string;
@@ -58,8 +58,21 @@ export declare const SESSION_RENEWAL_TIMEOUT_MS = 4000;
58
58
  * It binds the exact receipt bytes, profile, authority, workspace and the
59
59
  * locally synced profile revision, and lapses after a fixed window that the
60
60
  * file cannot extend. Anything unexpected is ignored and the authority is asked.
61
+ *
62
+ * Two kinds exist. A renewal refusal says only that this pin cannot be renewed
63
+ * as it is, so it is repeated only while migration is off. A refusal of the
64
+ * managed hook's migration itself (a project lock, a rotated epoch, a planner
65
+ * refusal or anything else that is not transient) also implies the renewal
66
+ * refusal, so it is repeated in either mode. Timeouts, an unavailable
67
+ * authority, write-lock contention and other transient failures are never
68
+ * remembered.
61
69
  */
62
70
  export declare const SESSION_RENEWAL_REFUSAL_TTL_MS: number;
71
+ /**
72
+ * Return the pending migration notice for this session once its pin holds the
73
+ * migrated profile, then mark it delivered. Anything unexpected returns null.
74
+ */
75
+ export declare function takeSessionPinMigrationNotice(sessionId: string, options?: SelectionCacheOptions): string | null;
63
76
  /**
64
77
  * Renew authorization for the complete immutable pin; never manufacture a
65
78
  * profile combining historical loaded versions with current unloaded versions.
@@ -67,6 +80,49 @@ export declare const SESSION_RENEWAL_REFUSAL_TTL_MS: number;
67
80
  * versions additionally require the authenticated exact-version bundle read.
68
81
  */
69
82
  export declare function reconcileSkillSessionIfSafe(sessionId: string, profileId: string, options?: SessionReconciliationOptions): Promise<boolean>;
83
+ /** Metadata only: what an automatic migration retired, never payloads. */
84
+ export interface SessionPinMigration {
85
+ profileId: string;
86
+ fromRevision: string;
87
+ toRevision: string;
88
+ retainedLoadedCount: number;
89
+ retiredLoadedCount: number;
90
+ retired: SessionSelectionDelta[];
91
+ archivePath: string;
92
+ receiptPath: string;
93
+ }
94
+ export type SessionPinRenewalOutcome = {
95
+ outcome: "unchanged";
96
+ } | {
97
+ outcome: "renewed";
98
+ } | {
99
+ outcome: "migrated";
100
+ migration: SessionPinMigration;
101
+ };
102
+ export interface SessionPinRenewalOptions extends SessionReconciliationOptions {
103
+ /**
104
+ * Managed hooks only: when renewal is refused because the same profile moved
105
+ * on, apply the explicit reconciliation plan to the authenticated current
106
+ * revision instead. Every other failure keeps its existing refusal.
107
+ */
108
+ migrate?: boolean;
109
+ /**
110
+ * Required for `migrate`: the prompt's directory (the hook input's cwd).
111
+ * Without it, or while it or an ancestor holds a project lock, migration
112
+ * does not run.
113
+ */
114
+ projectDir?: string;
115
+ }
116
+ /**
117
+ * A refused renewal whose automatic migration could not finish within the
118
+ * renewal deadline. It keeps the refusal code; the fixed `migration` marker
119
+ * lets the hook say why without carrying free text across its boundary.
120
+ */
121
+ export declare class SessionPinMigrationTimeout extends SkillSelectionError {
122
+ readonly migration: "timeout";
123
+ constructor();
124
+ }
125
+ export declare function renewSkillSessionPin(sessionId: string, profileId: string, options?: SessionPinRenewalOptions): Promise<SessionPinRenewalOutcome>;
70
126
  /** Explicit review for changed loaded selections or intentional profile migration. */
71
127
  export declare function reconcileSkillSession(input: SessionReconciliationInput, options?: SessionReconciliationOptions): Promise<{
72
128
  applied: false;