@hasna/skills 0.4.0 → 0.5.1
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 +220 -5
- package/bin/index.js +7747 -5645
- package/bin/mcp.js +1493 -431
- package/bin/migrate.js +148 -40
- package/bin/server.js +53 -83
- package/bin/worker.js +41 -73
- package/dist/admin-contract.d.ts +37 -19
- package/dist/admin-contract.js +1 -1
- package/dist/cli/cli.test-utils.d.ts +10 -8
- package/dist/cli/commands/customer-profile.d.ts +2 -0
- package/dist/cli/commands/customer-verification.d.ts +5 -0
- package/dist/cli/commands/tool-primitives.d.ts +1 -1
- package/dist/cli/commands/workspace-member-mutations.d.ts +2 -0
- package/dist/cli/commands/workspace-members.d.ts +2 -0
- package/dist/cli/commands/workspace-selection.d.ts +11 -0
- package/dist/cli/env-assignment.d.ts +9 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +841 -167
- package/dist/lib/agent-sync.d.ts +13 -8
- package/dist/lib/api-url.d.ts +4 -3
- package/dist/lib/app-home.d.ts +0 -1
- package/dist/lib/client-types.d.ts +75 -0
- package/dist/lib/credential-state.d.ts +12 -0
- package/dist/lib/fleet-credentials.d.ts +41 -15
- package/dist/lib/home-adoption.d.ts +2 -0
- package/dist/lib/home-census.d.ts +3 -1
- package/dist/lib/local-opt-in.d.ts +24 -0
- package/dist/lib/portable-skills-files.d.ts +6 -2
- package/dist/lib/read-access.d.ts +83 -0
- package/dist/lib/remote-auth.d.ts +23 -3
- package/dist/lib/remote-client.d.ts +45 -5
- package/dist/lib/remote-profile.d.ts +26 -0
- package/dist/lib/remote-registry.d.ts +7 -3
- package/dist/lib/remote-workspace-selection.d.ts +58 -0
- package/dist/lib/remote-workspace.d.ts +76 -0
- package/dist/lib/skillinfo.d.ts +1 -1
- package/dist/lib/workspace-profile.d.ts +49 -0
- package/dist/mcp/helpers.d.ts +22 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/sdk/governance-store.d.ts +1 -0
- package/dist/sdk/index.d.ts +8 -2
- package/dist/sdk/index.js +1312 -297
- package/dist/sdk/outputs.d.ts +0 -11
- package/dist/sdk/runs.d.ts +1 -1
- package/dist/storage.js +6 -40
- package/docs/skill-standard.md +30 -2
- package/package.json +6 -4
- package/dist/lib/instance-credentials-race.fixture.d.ts +0 -1
package/dist/lib/agent-sync.d.ts
CHANGED
|
@@ -29,9 +29,9 @@ export declare const SYNC_AGENTS: readonly SyncAgent[];
|
|
|
29
29
|
*/
|
|
30
30
|
export declare const SKILLS_SOURCE_ENV = "SKILLS_SOURCE";
|
|
31
31
|
/**
|
|
32
|
-
* Ownership marker written beside every SKILL.md this tool syncs. Its
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Ownership marker written beside every SKILL.md this tool syncs. Its exact managedBy
|
|
33
|
+
* value tells a re-sync which directories this tool owns; a foreign or malformed
|
|
34
|
+
* marker grants no authority. A hidden sidecar rather than a frontmatter field so the SKILL.md the
|
|
35
35
|
* agent loads stays exactly the adapted document and nothing else.
|
|
36
36
|
*/
|
|
37
37
|
export declare const SYNC_MARKER_FILE = ".hasna-skills.json";
|
|
@@ -42,6 +42,10 @@ export interface SyncMarker {
|
|
|
42
42
|
source: string;
|
|
43
43
|
syncedAt: string;
|
|
44
44
|
}
|
|
45
|
+
/** Internal shared ownership predicate; not exported by the public package entrypoint. */
|
|
46
|
+
export declare function isSkillsOwnershipMarker(marker: unknown): marker is Record<string, unknown>;
|
|
47
|
+
/** Read only a regular ownership sidecar. */
|
|
48
|
+
export declare function hasSkillsOwnershipMarker(dir: string): boolean;
|
|
45
49
|
export declare function isSyncAgent(value: string): value is SyncAgent;
|
|
46
50
|
export declare function resolveSyncAgents(arg?: string): SyncAgent[];
|
|
47
51
|
/** The global skills directory for an agent, honouring a test-supplied home. */
|
|
@@ -142,12 +146,12 @@ export interface WriteManagedAgentSkillParams {
|
|
|
142
146
|
/**
|
|
143
147
|
* Write one skill into one agent's global folder, non-clobbering.
|
|
144
148
|
*
|
|
145
|
-
* A directory this tool has written before carries
|
|
149
|
+
* A directory this tool has written before carries our exact ownership marker and is replaced with
|
|
146
150
|
* an exact mirror — except that a managed home holding full content is never silently
|
|
147
151
|
* replaced with an executable pointer stub (that would be data loss; it is refused
|
|
148
|
-
* unless `force` is passed). A directory with a SKILL.md but no marker is
|
|
149
|
-
*
|
|
150
|
-
*
|
|
152
|
+
* unless `force` is passed). A directory with a SKILL.md but no valid Skills marker is
|
|
153
|
+
* skipped unless `force` explicitly adopts it. Any other pre-existing unmanaged
|
|
154
|
+
* directory is always left untouched. A fresh directory is created.
|
|
151
155
|
*/
|
|
152
156
|
export declare function writeManagedAgentSkill(params: WriteManagedAgentSkillParams): AgentSyncAction;
|
|
153
157
|
export interface ManagedDirWriteResult {
|
|
@@ -173,6 +177,7 @@ export interface ManagedDirWriteOptions {
|
|
|
173
177
|
export declare function writeManagedSkillDir(dir: string, skillMd: string, options: ManagedDirWriteOptions): ManagedDirWriteResult;
|
|
174
178
|
/**
|
|
175
179
|
* Remove a skill this tool synced from an agent folder. Refuses to delete a directory it
|
|
176
|
-
* did not write (no
|
|
180
|
+
* did not write (no valid Skills ownership marker). Foreign or malformed markers
|
|
181
|
+
* grant no deletion authority, and removal has no force override.
|
|
177
182
|
*/
|
|
178
183
|
export declare function removeManagedAgentSkill(skill: string, agent: SyncAgent, homeDir?: string): boolean;
|
package/dist/lib/api-url.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* thin reading of `@hasna/contracts/client`. It exists only so the two failure
|
|
6
6
|
* MODES stay named:
|
|
7
7
|
*
|
|
8
|
-
* - Read paths fail closed: `resolveApiUrl()` returns `undefined`
|
|
9
|
-
*
|
|
10
|
-
* registry.
|
|
8
|
+
* - Read paths fail closed: `resolveApiUrl()` returns `undefined` only under
|
|
9
|
+
* the explicit local opt-in (`HASNA_SKILLS_LOCAL=1`, alias `SKILLS_LOCAL=1`),
|
|
10
|
+
* and the caller keeps working against the bundled local registry. Without
|
|
11
|
+
* the opt-in, an install with nothing configured is a refusal, not a URL.
|
|
11
12
|
* - Auth and write paths fail loudly: `requireApiUrl()` throws an error naming
|
|
12
13
|
* the missing configuration.
|
|
13
14
|
*
|
package/dist/lib/app-home.d.ts
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The @hasna/contracts client types that cross this package's published
|
|
3
|
+
* boundary, spelled locally.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. `bun build --target bun` inlines @hasna/contracts into
|
|
6
|
+
* every shipped bundle, so the runtime never needs it installed alongside
|
|
7
|
+
* @hasna/skills and it is correctly a build-time devDependency. `tsc
|
|
8
|
+
* --emitDeclarationOnly` inlines nothing: the moment a PUBLIC declaration names
|
|
9
|
+
* a type from `@hasna/contracts/client`, the published `.d.ts` imports it, and
|
|
10
|
+
* a consumer who installed only this package's declared runtime dependencies
|
|
11
|
+
* fails type-checking (7 x TS2307 before #1782 fixed the same shape in
|
|
12
|
+
* @hasna/secrets).
|
|
13
|
+
*
|
|
14
|
+
* This file is NOT the vendored resolver returning: it has no imports, no
|
|
15
|
+
* runtime statement, no tier, no Keychain read, no URL ladder — only the
|
|
16
|
+
* shapes, copied from the @hasna/contracts/@1.0.2 declarations. Its sibling
|
|
17
|
+
* `client-types.test.ts` asserts each one is mutually assignable with the real
|
|
18
|
+
* declaration, in the direction it actually crosses, so a drifted shape fails
|
|
19
|
+
* `tsc` in the same build step that emits the declarations it protects.
|
|
20
|
+
*
|
|
21
|
+
* `src/lib/fleet-credentials.ts` keeps importing the resolver and its VALUE
|
|
22
|
+
* exports from @hasna/contracts/client (inlined at runtime); only the types it
|
|
23
|
+
* re-exports or names in its own public signatures come from here.
|
|
24
|
+
*
|
|
25
|
+
* Do not add an import. Do not add a value. This file must stay a leaf.
|
|
26
|
+
*/
|
|
27
|
+
/** Which link of the chain supplied the credential. */
|
|
28
|
+
export type CredentialTier = "argument" | "override" | "pointer" | "profile" | "keychain" | "disk" | "env";
|
|
29
|
+
export interface ResolvedCredential {
|
|
30
|
+
/** The secret. Property access and destructuring work; it is never enumerated or serialized. */
|
|
31
|
+
readonly apiKey: string;
|
|
32
|
+
readonly tier: CredentialTier;
|
|
33
|
+
/** Where it came from: an env key NAME, an absolute file path, or a Keychain item reference. Never a value. */
|
|
34
|
+
readonly source: string;
|
|
35
|
+
/** True for tiers an operator sets on purpose. These never fall through. */
|
|
36
|
+
readonly deliberate: boolean;
|
|
37
|
+
/** When tier === "pointer", the vault ITEM KEY to resolve at request time. Never a credential value. */
|
|
38
|
+
readonly pointerVaultKey?: string;
|
|
39
|
+
/** The disk paths consulted before this credential was chosen. */
|
|
40
|
+
readonly diskCandidates: readonly string[];
|
|
41
|
+
/** Human-readable advisory. Never contains key material. */
|
|
42
|
+
readonly warning: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** The captured outcome of one `security` invocation. `stdout` IS the secret; it is never logged. */
|
|
45
|
+
export interface KeychainCommandResult {
|
|
46
|
+
/** Exit status; null when the tool could not be started or was killed. */
|
|
47
|
+
status: number | null;
|
|
48
|
+
stdout: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
}
|
|
51
|
+
/** Runs `/usr/bin/security` with the given argv — no shell. Injected by tests. */
|
|
52
|
+
export type KeychainCommandRunner = (argv: readonly string[]) => KeychainCommandResult;
|
|
53
|
+
/** Tier 3 controls. Every field is optional; production callers pass nothing. */
|
|
54
|
+
export interface KeychainTierOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Whether the Keychain is consulted for a caller-built env object. The tier
|
|
57
|
+
* is AMBIENT: by default it runs only for the live `process.env`. Injecting
|
|
58
|
+
* `run` implies `true`.
|
|
59
|
+
*/
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
/** Defaults to `process.platform`; the tier exists only on `"darwin"`. */
|
|
62
|
+
platform?: string;
|
|
63
|
+
/** The machine's host name, used as the account when `HASNA_STATION` is unset. */
|
|
64
|
+
hostname?: () => string;
|
|
65
|
+
/** The `security` runner. Defaults to spawning `/usr/bin/security` by argv. */
|
|
66
|
+
run?: KeychainCommandRunner;
|
|
67
|
+
}
|
|
68
|
+
export interface CredentialChainOptions {
|
|
69
|
+
/** Tier 1: an explicit key, e.g. from `--api-key`. */
|
|
70
|
+
apiKey?: string;
|
|
71
|
+
/** Tier 1: an explicit profile name, e.g. from `--profile`. Beats `HASNA_PROFILE`. */
|
|
72
|
+
profile?: string;
|
|
73
|
+
/** Tier 3: Keychain controls — a fake `security` runner in tests, an opt-out on CI. */
|
|
74
|
+
keychain?: KeychainTierOptions;
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CredentialState {
|
|
2
|
+
/** `misconfigured` is the ladder's refusal carried as data; `error` says why. */
|
|
3
|
+
mode: "hosted" | "local" | "misconfigured";
|
|
4
|
+
apiUrl: string | null;
|
|
5
|
+
apiUrlSource: string | null;
|
|
6
|
+
apiKeySource: string | null;
|
|
7
|
+
apiKeyTier: string | null;
|
|
8
|
+
credentialsFile: string | null;
|
|
9
|
+
credentialsFileMode: string | null;
|
|
10
|
+
error: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function describeCredentialState(): CredentialState;
|
|
@@ -36,8 +36,14 @@
|
|
|
36
36
|
* are not read anywhere else in this package. `SKILL_API_KEY` (singular) is
|
|
37
37
|
* gone: it shadowed nothing canonical and was never documented.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
39
|
+
* THIRD: the local run is opt-in only (owner ruling 2026-09-04, hasna/apps#1720;
|
|
40
|
+
* class-patch order 2026-09-06). `HASNA_SKILLS_LOCAL=1` (alias `SKILLS_LOCAL=1`)
|
|
41
|
+
* selects the on-machine run when the ENVIRONMENT configures no authority; it is
|
|
42
|
+
* answered before the resolver runs, so opting in never reads the Keychain or
|
|
43
|
+
* the credentials file. A configured environment always outranks it. The
|
|
44
|
+
* outcomes, and no fourth:
|
|
40
45
|
*
|
|
46
|
+
* - the local opt-in selected → LOCAL, announced once on stderr.
|
|
41
47
|
* - a credential resolves → HOSTED. The authority is the configured
|
|
42
48
|
* URL, else the fleet gateway. A credential
|
|
43
49
|
* that cannot produce a usable key — a
|
|
@@ -51,14 +57,18 @@
|
|
|
51
57
|
* There is no local fallback here: serving
|
|
52
58
|
* local results while authentication is
|
|
53
59
|
* unconfigured is a false green.
|
|
54
|
-
* - neither a credential nor a URL
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* - neither a credential nor a URL, and no opt-in
|
|
61
|
+
* → LOUD failure, exit non-zero, no SQLite,
|
|
62
|
+
* no *-local-fallback event. Running on
|
|
63
|
+
* this machine is a deliberate choice now,
|
|
64
|
+
* not the silence that follows a missing
|
|
65
|
+
* credential.
|
|
58
66
|
*/
|
|
59
|
-
import {
|
|
67
|
+
import type { CredentialChainOptions, CredentialTier, KeychainTierOptions, ResolvedCredential } from "./client-types.js";
|
|
60
68
|
/** The app slug: the Keychain service, the `~/.hasna/<app>` folder, the gateway path. */
|
|
61
69
|
export declare const SKILLS_APP = "skills";
|
|
70
|
+
/** The deliberate unhosted opt-in env names. Re-exported for the surfaces that have to name them. */
|
|
71
|
+
export { SKILLS_LOCAL_OPT_IN_ENV_KEYS, isSkillsLocalOptIn, selectsSkillsLocalMode } from "./local-opt-in.js";
|
|
62
72
|
type Env = Record<string, string | undefined>;
|
|
63
73
|
/** `HASNA_SKILLS_API_URL`, then the accepted `SKILLS_API_URL` alias. */
|
|
64
74
|
export declare const SKILLS_API_URL_ENV_KEYS: readonly string[];
|
|
@@ -129,6 +139,14 @@ export declare class SkillsFleetCredentialError extends Error {
|
|
|
129
139
|
readonly code: SkillsFleetErrorCode;
|
|
130
140
|
constructor(message: string, code?: SkillsFleetErrorCode);
|
|
131
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* True for this package's own refusal, across bundle boundaries.
|
|
144
|
+
*
|
|
145
|
+
* Exported for the surfaces that turn the refusal into data (an MCP tool's
|
|
146
|
+
* `AUTH_REQUIRED` result, a CLI handler's one-line stderr exit) so they match
|
|
147
|
+
* on the error's NAME rather than on a class identity a bundle may not share.
|
|
148
|
+
*/
|
|
149
|
+
export declare function isSkillsFleetCredentialError(error: unknown): error is SkillsFleetCredentialError;
|
|
132
150
|
/**
|
|
133
151
|
* Normalize a configured Skills authority to the origin the client dials.
|
|
134
152
|
*
|
|
@@ -164,10 +182,12 @@ export declare function skillsCredentialFilePath(env?: Env): string;
|
|
|
164
182
|
/**
|
|
165
183
|
* Say — once per process, on stderr — that this install is running locally.
|
|
166
184
|
*
|
|
167
|
-
* Local mode is legitimate for Skills: the corpus ships in the package. It is
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
185
|
+
* Local mode is legitimate for Skills: the corpus ships in the package. It is a
|
|
186
|
+
* deliberate choice now, though: it is reachable only through the explicit
|
|
187
|
+
* opt-in (`HASNA_SKILLS_LOCAL=1`), and it is still announced, because "no
|
|
188
|
+
* credential resolved" and "deliberately offline" look identical in the output
|
|
189
|
+
* otherwise, and the first one is usually a misconfiguration the operator wants
|
|
190
|
+
* to hear about.
|
|
171
191
|
*/
|
|
172
192
|
export declare function noticeLocalSkillsMode(write?: (line: string) => void): void;
|
|
173
193
|
/** Test seam: forget that the local-mode line was printed. */
|
|
@@ -203,10 +223,11 @@ export declare function requireSkillsApiKey(action?: string, env?: Env, options?
|
|
|
203
223
|
* The credential for a surface that reports refusals as data (an MCP tool, a
|
|
204
224
|
* `--json` command) rather than as an exception.
|
|
205
225
|
*
|
|
206
|
-
* `reason` is the ladder's own message when
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
226
|
+
* `reason` is the ladder's own message when a hosted resolution is refused — an
|
|
227
|
+
* authority with no key, or an unconfigured install without the local opt-in
|
|
228
|
+
* (fail-closed ruling) — a refusal carried as a value, NOT a fallback: the
|
|
229
|
+
* caller must still refuse. It is null only when the explicit local opt-in
|
|
230
|
+
* selected the on-machine run, the ordinary "not signed in" case.
|
|
210
231
|
*/
|
|
211
232
|
export declare function skillsCredentialOrReason(env?: Env, options?: SkillsFleetOptions): Promise<{
|
|
212
233
|
apiKey: string;
|
|
@@ -225,6 +246,12 @@ export declare function skillsCredentialOrReason(env?: Env, options?: SkillsFlee
|
|
|
225
246
|
* Keychain, credentials file), else the authority a resolved credential implies.
|
|
226
247
|
* With neither, this returns null and the caller fails loudly: R1 still holds,
|
|
227
248
|
* an install that named no service does not get to send an email address to one.
|
|
249
|
+
*
|
|
250
|
+
* The fail-closed refusal an unconfigured install now raises on every DATA
|
|
251
|
+
* surface is swallowed here and reported as plain "no authority": for a flow
|
|
252
|
+
* whose purpose is to acquire a credential, "nothing is configured" and "local
|
|
253
|
+
* mode is opted in" have the same two-step way out (configure an origin, then
|
|
254
|
+
* sign in).
|
|
228
255
|
*/
|
|
229
256
|
export declare function resolveSkillsApiOrigin(env?: Env, options?: SkillsFleetOptions): {
|
|
230
257
|
origin: string;
|
|
@@ -249,4 +276,3 @@ export declare class MissingSkillsFleetError extends Error {
|
|
|
249
276
|
readonly code = "MISSING_API_URL";
|
|
250
277
|
constructor(action?: string);
|
|
251
278
|
}
|
|
252
|
-
export {};
|
|
@@ -30,6 +30,8 @@ export interface AdoptionScan {
|
|
|
30
30
|
}
|
|
31
31
|
export interface AdoptionOptions extends PortableSkillOptions {
|
|
32
32
|
agents?: SyncAgent[];
|
|
33
|
+
/** Optional selected home/corpus names; omitted means all names. */
|
|
34
|
+
names?: string[];
|
|
33
35
|
/** Write markers and the conflicts ledger. Without it, scan only. */
|
|
34
36
|
apply?: boolean;
|
|
35
37
|
}
|
|
@@ -23,4 +23,6 @@ export interface DriftCensus {
|
|
|
23
23
|
managed: number;
|
|
24
24
|
clean: boolean;
|
|
25
25
|
}
|
|
26
|
-
export declare function censusHomeDrift(options?: AdoptionOptions
|
|
26
|
+
export declare function censusHomeDrift(options?: AdoptionOptions & {
|
|
27
|
+
names?: string[];
|
|
28
|
+
}): DriftCensus;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The deliberate unhosted opt-in, canonical name first. */
|
|
2
|
+
export declare const SKILLS_LOCAL_OPT_IN_ENV_KEYS: readonly ["HASNA_SKILLS_LOCAL", "SKILLS_LOCAL"];
|
|
3
|
+
export type SkillsLocalOptInEnv = Record<string, string | undefined>;
|
|
4
|
+
/** True when the operator deliberately asked for the unhosted on-machine run. */
|
|
5
|
+
export declare function isSkillsLocalOptIn(env?: SkillsLocalOptInEnv): boolean;
|
|
6
|
+
/** Every env name that can configure a Skills authority or credential, resolver-derived. */
|
|
7
|
+
export declare function skillsAuthorityEnvKeys(): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Does the ENVIRONMENT itself configure a Skills authority or credential?
|
|
10
|
+
*
|
|
11
|
+
* Deliberately narrower than "does a credential resolve": answering it must not
|
|
12
|
+
* touch the Keychain or the filesystem, because doing so would defeat the
|
|
13
|
+
* isolation the opt-in short-circuit exists to provide. It reads the env
|
|
14
|
+
* dictionary and nothing else.
|
|
15
|
+
*
|
|
16
|
+
* A DECLARED-BUT-BLANK variable counts as absent HERE — a blank has always been
|
|
17
|
+
* this package's spelling for "not configured", and helpers in the wild blank
|
|
18
|
+
* rather than delete. It is NOT absent once we do go hosted: the resolver
|
|
19
|
+
* refuses a blank loudly rather than falling through to another identity, which
|
|
20
|
+
* is the behaviour that matters at that point.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasSkillsEnvAuthorityIntent(env?: SkillsLocalOptInEnv): boolean;
|
|
23
|
+
/** True when this environment should be served by the on-box local run. */
|
|
24
|
+
export declare function selectsSkillsLocalMode(env?: SkillsLocalOptInEnv): boolean;
|
|
@@ -9,7 +9,11 @@ export declare function defaultRuntimeContract(entrypoint?: string): PortableSki
|
|
|
9
9
|
/** Provenance defaults; content_hash is filled at write time over the bundle. */
|
|
10
10
|
export declare function defaultProvenance(sourceCommit?: string): PortableSkillProvenance;
|
|
11
11
|
export declare function normalizePortableSkillName(name: string): string;
|
|
12
|
+
/** New local identities use hyphens; legacy lookups and command names keep their grammar. */
|
|
13
|
+
export declare function normalizeNewPortableSkillName(name: string): string;
|
|
12
14
|
export declare function readPortableSkillManifest(skillPath: string, fallbackName?: string): PortableSkillManifest;
|
|
15
|
+
/** Import needs the original spelling before the legacy reader discards case boundaries. */
|
|
16
|
+
export declare function readPortableSkillManifestForImport(skillPath: string): PortableSkillManifest;
|
|
13
17
|
export declare function parseSkillKind(value: string | undefined): SkillKind | undefined;
|
|
14
18
|
/**
|
|
15
19
|
* The version a skill EXPLICITLY declares, or undefined when none of the sources do:
|
|
@@ -46,8 +50,8 @@ export declare function ensurePortableSkillFiles(skillPath: string, manifest: Po
|
|
|
46
50
|
/**
|
|
47
51
|
* Instruction (prose) skills are consumed by agent renderers/MCP docs, not run
|
|
48
52
|
* locally, so `port` must never fabricate executable stubs (package.json, bin,
|
|
49
|
-
* src/index.ts, tsconfig.json, AGENTS.md).
|
|
50
|
-
*
|
|
53
|
+
* src/index.ts, tsconfig.json, AGENTS.md). Apart from aligning declared names
|
|
54
|
+
* after an import rename, it preserves the copied prose and metadata.
|
|
51
55
|
*/
|
|
52
56
|
export declare function ensureInstructionSkillFiles(skillPath: string, manifest: PortableSkillManifest): PortableSkillManifest;
|
|
53
57
|
export declare function copySkillDirectory(source: string, destination: string): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate every DATA surface runs before it answers from the bundled corpus,
|
|
3
|
+
* and the one registry those surfaces show.
|
|
4
|
+
*
|
|
5
|
+
* `skills list` has resolved through the fleet ladder since the fail-closed
|
|
6
|
+
* ruling (owner directive 2026-09-04, hasna/apps#1720): with no credential, no
|
|
7
|
+
* authority and no `HASNA_SKILLS_LOCAL=1` opt-in it exits 1 naming the refusal,
|
|
8
|
+
* opens nothing and serves nothing. `skills info` / `docs` / `requires`, the
|
|
9
|
+
* bare non-TTY listing and the MCP discovery tools (`list_skills`,
|
|
10
|
+
* `search_skills`, `get_skill_info`, `get_skill_docs`, `list_categories`,
|
|
11
|
+
* `list_tags`, `get_requirements`) did NOT — they read `loadRegistry()` and
|
|
12
|
+
* answered from the bundled catalog plus `~/.hasna/skills/installed` at exit 0
|
|
13
|
+
* with no notice, on the very machine `skills list` had just refused (#1720
|
|
14
|
+
* validation, round 1). Two surfaces that disagree about whether an install is
|
|
15
|
+
* configured is the false green the ruling removed, so they now share this one
|
|
16
|
+
* gate with the browsing commands.
|
|
17
|
+
*
|
|
18
|
+
* - refused (nothing configured, no opt-in; an authority with no key; a
|
|
19
|
+
* deliberate selection that cannot be honoured) → `SkillsFleetCredentialError`
|
|
20
|
+
* from the shared ladder, carried to the caller unchanged: the CLI prints
|
|
21
|
+
* its one line and exits 1, an MCP tool answers `AUTH_REQUIRED`.
|
|
22
|
+
* - the explicit local opt-in → `{ mode: "local" }`; the ladder has announced
|
|
23
|
+
* local mode once on stderr, and the caller serves the on-machine answer.
|
|
24
|
+
* - a credential resolves → `{ mode: "hosted", apiOrigin }`. The key is
|
|
25
|
+
* deliberately NOT returned here: a surface that only reads local data has
|
|
26
|
+
* no business holding it, and the surfaces that send it resolve it again
|
|
27
|
+
* at request time (`remoteRequestHeaders`, per call, completing a vault
|
|
28
|
+
* pointer if that is the tier).
|
|
29
|
+
*
|
|
30
|
+
* Resolved fresh on every call — the resolver contract — so a credential that
|
|
31
|
+
* appears, rotates or disappears mid-process is honoured by the next call.
|
|
32
|
+
*/
|
|
33
|
+
import { type SkillsFleetOptions } from "./fleet-credentials.js";
|
|
34
|
+
import { type SkillMeta } from "./registry.js";
|
|
35
|
+
type Env = Record<string, string | undefined>;
|
|
36
|
+
/** Where a read surface stands, once the ladder has let it through. Never carries a key. */
|
|
37
|
+
export type SkillsReadAccess = {
|
|
38
|
+
mode: "hosted";
|
|
39
|
+
apiOrigin: string;
|
|
40
|
+
} | {
|
|
41
|
+
mode: "local";
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Run the fail-closed routing preamble for a read surface.
|
|
45
|
+
*
|
|
46
|
+
* Throws {@link SkillsFleetCredentialError} when the ladder refuses; the
|
|
47
|
+
* caller must let that refusal out (as a non-zero exit or a structured error),
|
|
48
|
+
* never swallow it into a local answer.
|
|
49
|
+
*/
|
|
50
|
+
export declare function requireSkillsReadAccess(env?: Env, options?: SkillsFleetOptions): Promise<SkillsReadAccess>;
|
|
51
|
+
/**
|
|
52
|
+
* The registry a browsing surface should show — the CLI's `list` / `search` /
|
|
53
|
+
* `categories` / `tags` and the MCP discovery tools, from ONE implementation.
|
|
54
|
+
*
|
|
55
|
+
* The default read path is folder UNION cloud: whenever the install is pointed
|
|
56
|
+
* at a hosted instance (a resolved credential, and HASNA_SKILLS_API_URL for your
|
|
57
|
+
* own instance) the authenticated remote registry joins the local listing even
|
|
58
|
+
* without `--remote`. The explicit local opt-in keeps today's exact local
|
|
59
|
+
* output; an unconfigured or auth-missing install is a refusal, thrown from the
|
|
60
|
+
* shared ladder (fail-closed R1 — see mergeRemoteRegistry()).
|
|
61
|
+
*
|
|
62
|
+
* `--remote` used to REPLACE the local registry: `skills list --remote` returned exactly
|
|
63
|
+
* what the instance served and nothing else, so the bundled corpus and every skill the
|
|
64
|
+
* operator had written locally disappeared from the listing the moment they pointed the
|
|
65
|
+
* CLI at their own server. It now MERGES, under the precedence documented in
|
|
66
|
+
* src/lib/registry-merge.ts: custom > extension > local > remote > official, "whichever
|
|
67
|
+
* copy this machine would actually use wins the listing".
|
|
68
|
+
*
|
|
69
|
+
* The profile (`all` vs the curated basic set) applies to the local half only. The
|
|
70
|
+
* instance's skills are never filtered by it: the basic profile is a hand-written list of
|
|
71
|
+
* ten bundled names, so applying it to remote entries would drop every published skill
|
|
72
|
+
* from `skills list --remote` - the same disappearance this change exists to fix.
|
|
73
|
+
*
|
|
74
|
+
* A remote failure is still fatal. An explicit `--remote` request (and a configured,
|
|
75
|
+
* authenticated default read) that fails surfaces a clear error, and silently returning
|
|
76
|
+
* the local half of a merge the user asked to include the remote half in would report
|
|
77
|
+
* success for a listing that is missing entries.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getBrowseRegistry(options?: {
|
|
80
|
+
all?: boolean;
|
|
81
|
+
remote?: boolean;
|
|
82
|
+
}): Promise<SkillMeta[]>;
|
|
83
|
+
export {};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { type RemoteWorkspaceContext, type RemoteWorkspaceSession, type RemoteAccountWorkspaceDiscovery } from "./remote-workspace-selection.js";
|
|
2
|
+
import { type RemoteWorkspaceMembersOptions } from "./remote-workspace.js";
|
|
3
|
+
import { type SetRemoteWorkspaceMemberRole, type RemoveRemoteWorkspaceMember } from "./remote-workspace.js";
|
|
4
|
+
import { type UpdateRemoteProfile, type UpdateRemoteWorkspace } from "./remote-profile.js";
|
|
1
5
|
export declare class HostedApiError extends Error {
|
|
2
6
|
readonly status?: number;
|
|
3
7
|
readonly code?: string;
|
|
@@ -21,12 +25,28 @@ export declare class RemoteSkillsAuthClient {
|
|
|
21
25
|
startDevice(): Promise<any>;
|
|
22
26
|
pollDevice(deviceCode: string): Promise<any>;
|
|
23
27
|
private sessionClient;
|
|
24
|
-
|
|
28
|
+
/** Discover memberships with fresh sign-in. No key or session is saved. */
|
|
29
|
+
listAccountWorkspaces(email: string, code: string, expectedUserId?: string): Promise<RemoteAccountWorkspaceDiscovery>;
|
|
30
|
+
/** Contains a secret session token. Selection never creates or stores an API key. */
|
|
31
|
+
switchWorkspace(email: string, code: string, context: RemoteWorkspaceContext): Promise<RemoteWorkspaceSession>;
|
|
32
|
+
private workspaceLogin;
|
|
33
|
+
createApiKey(email: string, code: string, name: string, scopes?: string[], context?: RemoteWorkspaceContext): Promise<{
|
|
25
34
|
[field: string]: unknown;
|
|
26
35
|
key: string;
|
|
27
36
|
}>;
|
|
28
|
-
listApiKeys(email: string, code: string): Promise<Record<string, unknown>[]>;
|
|
29
|
-
revokeApiKey(email: string, code: string, keyId: string): Promise<Record<string, unknown>>;
|
|
37
|
+
listApiKeys(email: string, code: string, context?: RemoteWorkspaceContext): Promise<Record<string, unknown>[]>;
|
|
38
|
+
revokeApiKey(email: string, code: string, keyId: string, context?: RemoteWorkspaceContext): Promise<Record<string, unknown>>;
|
|
39
|
+
/** Reauthentication is ephemeral: it never replaces a saved key or profile. */
|
|
40
|
+
updateProfile(email: string, code: string, input: UpdateRemoteProfile, context?: RemoteWorkspaceContext): Promise<{
|
|
41
|
+
user: import("./remote-profile.js").RemoteCustomerProfile;
|
|
42
|
+
}>;
|
|
43
|
+
updateCurrentWorkspace(email: string, code: string, input: UpdateRemoteWorkspace, context?: RemoteWorkspaceContext): Promise<{
|
|
44
|
+
organization: import("./remote-profile.js").RemoteCurrentWorkspace;
|
|
45
|
+
}>;
|
|
46
|
+
/** Fresh owner/admin session; explicit context survives default-workspace OTP selection. */
|
|
47
|
+
listWorkspaceMembers(email: string, code: string, options?: RemoteWorkspaceMembersOptions, context?: RemoteWorkspaceContext): Promise<import("./remote-workspace.js").RemoteWorkspaceMembersPage>;
|
|
48
|
+
setWorkspaceMemberRole(email: string, code: string, membershipId: string, input: SetRemoteWorkspaceMemberRole, context?: RemoteWorkspaceContext): Promise<import("./remote-workspace.js").RemoteWorkspaceMemberRoleResult>;
|
|
49
|
+
removeWorkspaceMember(email: string, code: string, membershipId: string, input: RemoveRemoteWorkspaceMember, context?: RemoteWorkspaceContext): Promise<import("./remote-workspace.js").RemoteWorkspaceMemberRemovalResult>;
|
|
30
50
|
/** Common auth transport used by CLI login, preserving the selected instance through awaits. */
|
|
31
51
|
request(path: string, options?: RequestInit): Promise<any>;
|
|
32
52
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { type RemoteWorkspaceContext, type RemoteAccountWorkspaces, type RemoteWorkspaceSession, type RemoteWorkspaceSelectionErrorCode } from "./remote-workspace-selection.js";
|
|
2
|
+
import { type RemoteWorkspaceMembersOptions, type RemoteWorkspaceMembersPage } from "./remote-workspace.js";
|
|
3
|
+
import { type RemoteWorkspaceMemberErrorCode, type SetRemoteWorkspaceMemberRole, type RemoveRemoteWorkspaceMember, type RemoteWorkspaceMemberRoleResult, type RemoteWorkspaceMemberRemovalResult } from "./remote-workspace.js";
|
|
1
4
|
import { type RemoteSkillRunContract } from "./remote-run-contract.js";
|
|
2
5
|
import { type RemoteCreditPack, type RemoteRunApproval, type RemoteRunQuote } from "./remote-account.js";
|
|
3
6
|
import { type RemoteInputFile } from "./remote-files.js";
|
|
7
|
+
import { type UpdateRemoteProfile, type UpdateRemoteWorkspace } from "./remote-profile.js";
|
|
4
8
|
/**
|
|
5
9
|
* A server that predates this client's pin/tag/incremental-sync routes answered
|
|
6
10
|
* 404/405 for them. The caller must never mistake that for "no pins" or "empty
|
|
@@ -17,7 +21,22 @@ export declare class RemoteRouteUnsupportedError extends Error {
|
|
|
17
21
|
export declare class RemoteRequestError extends Error {
|
|
18
22
|
readonly path: string;
|
|
19
23
|
readonly status: number;
|
|
20
|
-
constructor(path: string, status: number,
|
|
24
|
+
constructor(path: string, status: number, _statusText?: string);
|
|
25
|
+
}
|
|
26
|
+
/** A recognized membership refusal, with fixed text and no server payload. */
|
|
27
|
+
export declare class RemoteWorkspaceMemberError extends RemoteRequestError {
|
|
28
|
+
readonly code: RemoteWorkspaceMemberErrorCode;
|
|
29
|
+
constructor(path: string, code: RemoteWorkspaceMemberErrorCode);
|
|
30
|
+
}
|
|
31
|
+
/** Fixed text for recognized workspace refusals; no reflected server error payload. */
|
|
32
|
+
export declare class RemoteWorkspaceSelectionError extends RemoteRequestError {
|
|
33
|
+
readonly code: RemoteWorkspaceSelectionErrorCode;
|
|
34
|
+
constructor(path: string, code: RemoteWorkspaceSelectionErrorCode);
|
|
35
|
+
}
|
|
36
|
+
/** A recognized unavailable capability; all displayed text is client-owned. */
|
|
37
|
+
export declare class RemoteCapabilityUnavailableError extends RemoteRequestError {
|
|
38
|
+
readonly code: "SUBSCRIPTION_CHECKOUT_UNAVAILABLE";
|
|
39
|
+
constructor();
|
|
21
40
|
}
|
|
22
41
|
/**
|
|
23
42
|
* A remote pin on a skill, matching the hosted-pins wire shape
|
|
@@ -106,6 +125,26 @@ export declare class RemoteSkillsClient {
|
|
|
106
125
|
/** Quote first and fail closed when the caller has not approved the required credits. */
|
|
107
126
|
submitQuotedRun(slug: string, input?: Record<string, unknown>, args?: string[], approval?: RemoteRunApproval): Promise<RemoteSkillRunContract>;
|
|
108
127
|
getIdentity(): Promise<Record<string, unknown>>;
|
|
128
|
+
/** List memberships with the current interactive session; never writes credentials. */
|
|
129
|
+
listAccountWorkspaces(expectedUserId?: string): Promise<RemoteAccountWorkspaces>;
|
|
130
|
+
/** Return a new ephemeral session; this client and any saved key/profile stay unchanged. */
|
|
131
|
+
switchWorkspace(context: RemoteWorkspaceContext): Promise<RemoteWorkspaceSession>;
|
|
132
|
+
private requestWorkspaceSelection;
|
|
133
|
+
/** Requires a customer session; API keys and support impersonation cannot edit names. */
|
|
134
|
+
updateProfile(input: UpdateRemoteProfile): Promise<{
|
|
135
|
+
user: import("./remote-profile.js").RemoteCustomerProfile;
|
|
136
|
+
}>;
|
|
137
|
+
/** Owner/admin session only; the current workspace identity and slug stay fixed. */
|
|
138
|
+
updateCurrentWorkspace(input: UpdateRemoteWorkspace): Promise<{
|
|
139
|
+
organization: import("./remote-profile.js").RemoteCurrentWorkspace;
|
|
140
|
+
}>;
|
|
141
|
+
/** Current owner/admin customer session only; the server refuses API keys and impersonation. */
|
|
142
|
+
listWorkspaceMembers(options?: RemoteWorkspaceMembersOptions): Promise<RemoteWorkspaceMembersPage>;
|
|
143
|
+
/** Exact incarnation and expected role; no refresh or retry. Server enforces current authority. */
|
|
144
|
+
setWorkspaceMemberRole(membershipId: string, input: SetRemoteWorkspaceMemberRole): Promise<RemoteWorkspaceMemberRoleResult>;
|
|
145
|
+
/** Removes only this incarnation. A successful tombstone replay is returned unchanged. */
|
|
146
|
+
removeWorkspaceMember(membershipId: string, input: RemoveRemoteWorkspaceMember): Promise<RemoteWorkspaceMemberRemovalResult>;
|
|
147
|
+
private requestWorkspaceMember;
|
|
109
148
|
listApiKeys(): Promise<Record<string, unknown>[]>;
|
|
110
149
|
createApiKey(name: string, scopes?: string[]): Promise<{
|
|
111
150
|
key: string;
|
|
@@ -211,11 +250,12 @@ export declare class RemoteSkillsClient {
|
|
|
211
250
|
}
|
|
212
251
|
/**
|
|
213
252
|
* The client for the configured instance, or null when this install runs on
|
|
214
|
-
* this machine
|
|
253
|
+
* this machine — which is now the explicit local opt-in only
|
|
254
|
+
* (`HASNA_SKILLS_LOCAL=1`); with no credential, no authority and no opt-in the
|
|
255
|
+
* shared ladder throws (fail-closed ruling), so the caller fails loudly instead
|
|
256
|
+
* of quietly reading the bundled corpus while authentication is unconfigured.
|
|
215
257
|
*
|
|
216
|
-
* A configured authority with no credential
|
|
217
|
-
* ladder throws, so the caller fails loudly instead of quietly reading the
|
|
218
|
-
* bundled corpus while authentication is unconfigured.
|
|
258
|
+
* A configured authority with no credential also throws for the same reason.
|
|
219
259
|
*
|
|
220
260
|
* ASYNC because the credential ladder is: a vault pointer
|
|
221
261
|
* (`HASNA_SKILLS_API_KEY_REF`) is completed through the secrets vault before a
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type RemoteCustomerRole = "owner" | "admin" | "member" | "viewer";
|
|
2
|
+
export type RemoteCustomerProfile = {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
displayName: string | null;
|
|
6
|
+
role: RemoteCustomerRole;
|
|
7
|
+
};
|
|
8
|
+
export type RemoteCurrentWorkspace = {
|
|
9
|
+
id: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
export type UpdateRemoteProfile = {
|
|
14
|
+
displayName: string;
|
|
15
|
+
};
|
|
16
|
+
export type UpdateRemoteWorkspace = {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
/** Client input checks do not grant authority; the selected server owns policy. */
|
|
20
|
+
export declare function customerNamePatch(input: unknown, field: "displayName" | "name"): Record<string, string>;
|
|
21
|
+
export declare function parseUpdatedProfile(value: unknown): {
|
|
22
|
+
user: RemoteCustomerProfile;
|
|
23
|
+
};
|
|
24
|
+
export declare function parseUpdatedWorkspace(value: unknown): {
|
|
25
|
+
organization: RemoteCurrentWorkspace;
|
|
26
|
+
};
|
|
@@ -45,9 +45,13 @@ export declare function loadRemoteRegistry(options?: RemoteRegistryOptions): Pro
|
|
|
45
45
|
* origin sees the folder UNION cloud in the plain `list`/`search` path, while
|
|
46
46
|
* every other install keeps today's exact local behavior.
|
|
47
47
|
*
|
|
48
|
-
* - Nothing configured
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* - Nothing configured, local opted in -> the local list is returned
|
|
49
|
+
* unchanged and no request is attempted. An install running on this
|
|
50
|
+
* machine must stay byte-identical to the pre-merge output.
|
|
51
|
+
* - Nothing configured and NO local opt-in -> this throws, from the shared
|
|
52
|
+
* ladder (MISSING_API_CREDENTIAL, naming `HASNA_SKILLS_LOCAL` as the
|
|
53
|
+
* deliberate way out): local mode is opt-in only, and an unconfigured
|
|
54
|
+
* install is a refusal rather than a silent local listing.
|
|
51
55
|
* - An authority configured with NO credential -> this throws, from the
|
|
52
56
|
* shared ladder. It used to return the local half silently, which is the
|
|
53
57
|
* false green the 2026-09-04 ruling removes: an operator who pointed this
|