@hasna/skills 0.5.1 → 0.5.3

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
@@ -674,7 +674,7 @@ skills/ # Public skill contracts and local OSS skills
674
674
  |---|---|---|
675
675
  | Catalog skills | 86 | `SKILLS.length` (`src/lib/registry-data/`) |
676
676
  | Categories | 17 | `CATEGORIES` (`src/lib/registry-types.ts`) |
677
- | MCP tools | 61 | `tools/list` against a live `buildServer()` |
677
+ | MCP tools | 68 | `tools/list` against a live `buildServer()` |
678
678
 
679
679
  Every number in this table is re-derived from the source tree on each test run by
680
680
  `src/lib/readme-derived-counts.test.ts`, so a drifted figure fails a test rather
@@ -933,3 +933,198 @@ through a tool remains a separate follow-up.
933
933
  If enrollment reports that key issuance was attempted but not confirmed, inspect
934
934
  the selected profile and workspace keys before retrying. A lost response can
935
935
  still have created a server key; the CLI does not retry issuance automatically.
936
+
937
+ ### Leave a workspace
938
+
939
+ Use the exact membership ID and observed role from fresh `workspace list` output.
940
+ Leaving requires deliberate confirmation and a fresh verification code:
941
+
942
+ ```sh
943
+ HASNA_PROFILE=team-b skills workspace leave <membership-id> --expected-role member --email you@example.com --code-stdin --confirm --json
944
+ ```
945
+
946
+ The selected profile must authenticate the same membership. Without a named
947
+ profile, supply `--user-id <user-id>` from discovery; this also supports viewers
948
+ who cannot create API keys. The server refuses stale roles, the last active
949
+ owner, and leaving your last available workspace. It decides authority atomically.
950
+
951
+ The SDK offers `RemoteSkillsAuthClient.leaveWorkspace(email, code,
952
+ { userId, membershipId }, { expectedRole, confirm: true })`; an existing interactive
953
+ session can use `RemoteSkillsClient.leaveWorkspace(context, input)`. MCP exposes
954
+ `leave_workspace` with those same explicit IDs, role, confirmation and fresh code.
955
+ All surfaces call the same HTTP method once. API keys cannot authorize the leave.
956
+
957
+ Success returns `{ organizationId, membershipId, removed: true,
958
+ signInRequired: true }`. Sign in again to an available workspace afterwards.
959
+ Saved credentials and unrelated profiles remain unchanged; credentials for the
960
+ left membership no longer grant access. A lost or invalid response raises
961
+ `RemoteWorkspaceLeaveUnconfirmedError`: inspect available memberships before any
962
+ new action. Never automatically retry or substitute another membership ID.
963
+
964
+
965
+ ## Workspace invitations
966
+
967
+ The SDK, CLI and MCP use the same invitation operations on your configured API.
968
+ A compatible hosted service must enable invitation delivery. Every operation
969
+ requires fresh verification bound to your observed user ID and current membership
970
+ ID; any named profile must match. Owners can invite all roles; admins manage only
971
+ member/viewer invitations. The server enforces current authority and verified
972
+ recipient email. Existing accounts with no available membership still need the
973
+ separate invitation recovery flow; these commands do not bypass ordinary login.
974
+
975
+ Use `skills workspace list` to observe the account and membership IDs. The
976
+ `workspace invitations` commands are `list`, `get <invitation-id>`, `issue`,
977
+ `resend <invitation-id>`, `revoke <invitation-id>` and `accept <invitation-id>`.
978
+ Each requires `--email`, `--user-id` and `--membership-id`. Mutations also require
979
+ `--confirm`. `list --after <nextCursor>` reads one additional page, at most 50
980
+ invitations; it never automatically traverses the account.
981
+
982
+ For `issue`, supply `--recipient`, `--role` and your own
983
+ `--idempotency-key <uuid>`. `resend` requires that key and
984
+ `--expected-generation`; `revoke` requires `--expected-generation`. Save the
985
+ request key with its original nonsecret context and parameters before issuing or
986
+ resending. A lost response is not proof of failure: read current invitations or
987
+ reconcile with exactly the same key and original parameters. Never replace the
988
+ key to retry an uncertain action. No command retries or rewrites saved profiles.
989
+
990
+ Read, issue, resend and revoke can read a previously requested six-digit code via
991
+ `--code-stdin`. Otherwise an interactive terminal requests a code and masks input.
992
+ Acceptance takes its invitation ID as the argument and reads the fresh code on
993
+ stdin line one and the 43-character invitation token on line two with
994
+ `--secrets-stdin`. Interactive acceptance masks both inputs. Do not put either
995
+ secret in arguments, environment variables, shell history, scripts or profiles.
996
+ Obtain both from your inbox and pass them through your terminal or an approved
997
+ secret-input mechanism. JSON/noninteractive acceptance requires `--secrets-stdin`.
998
+
999
+ Acceptance returns the joined organization and membership IDs. It does not change
1000
+ the current workspace, default home, saved keys or profiles. Inspect your workspace
1001
+ list and deliberately select a membership afterward. A delivery state of
1002
+ `provider_accepted` means the provider accepted the request, not that it reached
1003
+ an inbox.
1004
+
1005
+ Both SDK entrypoints export `RemoteSkillsClient` methods
1006
+ `listWorkspaceInvitations(context, options?)`,
1007
+ `getWorkspaceInvitation(context, invitationId)`,
1008
+ `issueWorkspaceInvitation(context, input)`,
1009
+ `resendWorkspaceInvitation(context, invitationId, input)`,
1010
+ `revokeWorkspaceInvitation(context, invitationId, input)` and
1011
+ `acceptWorkspaceInvitation(context, invitationId, { token, confirm: true })`.
1012
+ The corresponding `RemoteSkillsAuthClient` methods prepend `email, code` to those
1013
+ arguments for fresh verification. `context` is `{ userId, membershipId }`.
1014
+ Issue input is `{ email, role, idempotencyKey, confirm: true }`; resend is
1015
+ `{ expectedGeneration, idempotencyKey, confirm: true }`; revoke is
1016
+ `{ expectedGeneration, confirm: true }`. Tokens and temporary sessions are never
1017
+ returned in invitation projections or persisted by these methods.
1018
+
1019
+ MCP exposes `list_workspace_invitations`, `get_workspace_invitation`,
1020
+ `issue_workspace_invitation`, `resend_workspace_invitation`,
1021
+ `revoke_workspace_invitation` and `accept_workspace_invitation`. Supply explicit
1022
+ `userId`, `membershipId`, `email` and fresh `code` in each tool request. Issue uses
1023
+ `recipient` for the invitation email; acceptance's `token` travels only in the
1024
+ MCP request. Treat the host's request history as sensitive; the tools never echo
1025
+ that token or save it in a profile. Other mutation fields match the SDK.
1026
+
1027
+ Known server refusals are fixed `RemoteWorkspaceInvitationError` codes.
1028
+ `WorkspaceInvitationInputError` rejects invalid inputs before authentication.
1029
+ `RemoteWorkspaceInvitationReadError` refuses malformed, cross-workspace or
1030
+ unbounded results. `RemoteWorkspaceInvitationUnconfirmedError` requires explicit
1031
+ reconciliation of a mutation; it never triggers automatic replay. These adapters
1032
+ require service and real recipient acceptance testing before invitations can be
1033
+ offered as a live product capability.
1034
+
1035
+ ### Content identity without extraction
1036
+
1037
+ `computeContentHashFromEntries` and `verifyContentHashFromEntries` are available
1038
+ from the root package and `@hasna/skills/sdk`. They consume regular-file entries
1039
+ such as the result of `inspectSkillBundle`, revalidate their shape and paths, and
1040
+ copy their bytes before yielding. Neither function reads a directory, extracts
1041
+ files, changes the supplied entries, or authorizes code execution.
1042
+
1043
+ ```ts
1044
+ import { inspectSkillBundle, computeContentHashFromEntries,
1045
+ verifyContentHashFromEntries, revisionIdOf } from "@hasna/skills/sdk";
1046
+
1047
+ const inspected = await inspectSkillBundle(uploadBytes, { signal });
1048
+ const contentHash = await computeContentHashFromEntries(inspected.entries, { signal });
1049
+ const verification = await verifyContentHashFromEntries(inspected.entries, { signal });
1050
+ ```
1051
+
1052
+ Verification reads `provenance.content_hash` from the same captured `skill.json`;
1053
+ it cannot accept a second manifest in place of those bytes. Manifest field
1054
+ validation remains a separate operation. `revisionIdOf` and `RevisionContent`
1055
+ expose the existing revision identity, whose optional fields default to null and
1056
+ whose published tag order matters. An archive SHA, canonical content hash and
1057
+ published revision ID describe different identities and are not interchangeable.
1058
+
1059
+ The entry functions preserve the directory hash's coverage, LF normalization,
1060
+ binary handling, manifest self-hash removal, ordering and serialization. The
1061
+ archive packer has a different exclusion policy; hashing inspected entries does
1062
+ not add excluded files back into an archive. Existing directory functions remain
1063
+ synchronous and retain their previous behavior.
1064
+
1065
+ `CONTENT_HASH_LIMITS` caps the entry count at 1,024, raw and normalized content
1066
+ at 64 MiB, each raw/normalized file at 16 MiB, paths at 100 UTF-8 bytes, and the root
1067
+ manifest at 16 KiB with nesting depth 64. The deadline is five seconds. Callers may lower
1068
+ these limits through `ContentHashOptions`; zero and values above the ceilings
1069
+ are refused. Excluded entries still count toward raw limits and path-collision
1070
+ checks. Paths must be canonical and distinct under NFC/case folding; shared
1071
+ buffers and accessor-backed entry fields are refused. This validates ordinary
1072
+ untrusted byte entries, not arbitrary JavaScript proxies or hostile host code.
1073
+ `ContentHashInputError` reports fixed invalid-input, limit, abort or timeout codes
1074
+ without including file contents or paths in its messages.
1075
+
1076
+ ## Invitation email recovery
1077
+
1078
+ An existing account with no usable membership can explicitly recover access by
1079
+ accepting an invitation with its secret and a fresh recovery code. This separate
1080
+ flow creates no session, key, workspace, default pointer or credit grant. New
1081
+ accounts use ordinary signup. A compatible service must enable email recovery.
1082
+
1083
+ Set `HASNA_SKILLS_API_URL` (or `SKILLS_API_URL`) explicitly for recovery. If both
1084
+ are set, they must name the same API. Recovery never consults a keychain or saved
1085
+ key to choose its server. It preserves all profiles and never treats cached
1086
+ identity metadata as proof.
1087
+
1088
+ Generate and retain a challenge UUID **before** the first request. Keep it with
1089
+ the same invitation ID and server, without storing the token or code. Run
1090
+ `skills workspace invitations email-challenge <invitation-id>
1091
+ --challenge-id <your-uuid> --confirm` to enter the invitation token through a
1092
+ masked prompt, or add `--token-stdin` for exactly one secret input line.
1093
+ The eligibility-neutral result does not confirm that a code was sent or delivered.
1094
+ A reused challenge ID never requests a replacement code automatically.
1095
+
1096
+ After receiving the recovery code, run
1097
+ `skills workspace invitations email-accept <invitation-id>
1098
+ --challenge-id <same-uuid> --confirm`. Interactive entry masks the token and code;
1099
+ `--secrets-stdin` reads the recovery code on line one and the invitation token on
1100
+ line two. JSON use requires the corresponding stdin option. Never put proof in
1101
+ arguments, environment variables, URLs, scripts or shell history.
1102
+
1103
+ Acceptance returns `accepted: true`, `changed: true`, `signInRequired: true` and
1104
+ the organization/membership IDs. Use fresh ordinary sign-in afterward. If the
1105
+ acceptance response is lost, sign in to inspect available memberships before any
1106
+ further action; never retry acceptance automatically. If necessary, deliberately
1107
+ request a new recovery challenge. An uncertain challenge response requires
1108
+ retaining its original ID and checking your inbox, without automatic rotation.
1109
+
1110
+ Both SDK entrypoints expose `RemoteSkillsAuthClient` methods
1111
+ `requestInvitationEmailChallenge({ invitationId, token, challengeId, confirm: true })`
1112
+ and `acceptInvitationEmailChallenge({ invitationId, token, challengeId, code,
1113
+ confirm: true })`. Both send one anonymous POST to the captured API origin and
1114
+ return bounded, validated projections. `InvitationEmailInputError` rejects invalid
1115
+ input, `RemoteInvitationEmailError` exposes fixed service refusal codes, and
1116
+ `RemoteInvitationEmailUnconfirmedError` requires explicit reconciliation. No
1117
+ cookie, Authorization header, credential resolution, login or profile write is
1118
+ part of these methods.
1119
+
1120
+ For an agent host without a key, start
1121
+ `skills-mcp --invitation-recovery --stdio` with an explicit API URL. This mode
1122
+ exposes only `request_invitation_email_challenge` and
1123
+ `accept_invitation_email_challenge`, with the same SDK input fields. It rejects
1124
+ other startup flags, HTTP mode and local mode. The ordinary MCP startup and data
1125
+ access gates remain in effect outside this mode. Treat host request history as
1126
+ sensitive: tokens and codes appear only in the MCP input body and are never
1127
+ returned or saved by the tools.
1128
+
1129
+ These clients still require deployed configuration and controlled real recipient
1130
+ email acceptance before recovery can be offered as a live product capability.