@huynle/hub 0.6.1 → 0.7.2
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/CHANGELOG.md +870 -0
- package/Dockerfile +33 -0
- package/README.md +1007 -66
- package/package.json +47 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +156 -0
- package/packages/cli/dist/hub-cli.js +21647 -0
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +123 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +252 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +104 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +95 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +553 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +107 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +70 -0
- package/packages/sdk/src/types.ts +517 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +101 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +152 -0
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +328 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +74 -0
- package/src/central/agent/escalation.ts +219 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +385 -0
- package/src/central/agent/routes.ts +281 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +112 -0
- package/src/central/agent/webhook-routes.ts +86 -0
- package/src/central/agent/webhooks.ts +493 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +350 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +116 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +246 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1149 -170
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/mcp-http.ts +116 -0
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +625 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +118 -7
- package/src/index.ts +195 -43
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +174 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +17 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Grants (design §5, §7; decision wvqeuis0; exploration hcw63slg): what lets a session or app token act for its
|
|
2
|
+
// user. Storage, holder/tenancy checks and the hourly cap; the policy is policy.ts, the check order acting.ts.
|
|
3
|
+
//
|
|
4
|
+
// - Stored per owner in the StateStore (collection "grants", keyed by id `g_<16 hex>`). Only the user creates,
|
|
5
|
+
// edits or revokes one (grant-routes.ts admits user actors only).
|
|
6
|
+
// - Holders: `ses_…` and `@name` are pinned to the session they name at creation (a grant never follows a later
|
|
7
|
+
// holder of the name); a reserved `@@role` follows the role (whoever holds it when the grant is used); an
|
|
8
|
+
// `app:<id>` holder must be one of the owner's app tokens. All must belong to the owner, at creation and on use.
|
|
9
|
+
// - `expires` becomes an absolute expiresAt; revoke ends a grant (kept, for the audit's "grant g_12"). Ended
|
|
10
|
+
// grants beyond GRANTS_KEPT are dropped, oldest first.
|
|
11
|
+
// - Use (acting.ts): usable() answers the active grants an actor holds for a verb on a session directory (scope:
|
|
12
|
+
// canonical project paths on path boundaries), re-checking owner, holder and token every time; consume() counts
|
|
13
|
+
// an action against maxActionsPerHour in fixed clock-hour windows (Counters), rate_limited past it.
|
|
14
|
+
// - The admin audit gets only {actor: owner, action: grant.*, target: id}, never holder, scope or permissions.
|
|
15
|
+
import { randomBytes } from "node:crypto"
|
|
16
|
+
import type { Counters, StateStore } from "../backend/types"
|
|
17
|
+
import type { AuditEntry, Store } from "../store"
|
|
18
|
+
import type { Directory } from "./directory"
|
|
19
|
+
import { AgentError } from "./errors"
|
|
20
|
+
import type { AgentActor, AppActor, SessionActor } from "./actor"
|
|
21
|
+
import { type GrantSpec, type GrantVerb, inProjects, validateGrant } from "./policy"
|
|
22
|
+
import { updateVersioned } from "./versioned"
|
|
23
|
+
|
|
24
|
+
/** Active (unexpired, unrevoked) grants per user. */
|
|
25
|
+
export const GRANTS_ACTIVE_MAX = 50
|
|
26
|
+
/** Ended (expired or revoked) grants each user keeps for the audit trail. */
|
|
27
|
+
export const GRANTS_KEPT = 200
|
|
28
|
+
|
|
29
|
+
const COLLECTION = "grants"
|
|
30
|
+
const HOUR_MS = 3_600_000
|
|
31
|
+
const ID = /^g_[0-9a-f]{16}$/
|
|
32
|
+
|
|
33
|
+
export type GrantState = "active" | "expired" | "revoked"
|
|
34
|
+
|
|
35
|
+
export interface Grant {
|
|
36
|
+
readonly id: string
|
|
37
|
+
readonly owner: string
|
|
38
|
+
/** As the user wrote it: `@@role`, `@name`, `ses_…` or `app:<token id>`. */
|
|
39
|
+
readonly holder: string
|
|
40
|
+
/** The session a `ses_…` or `@name` holder was pinned to at creation. */
|
|
41
|
+
readonly holderSessionID?: string
|
|
42
|
+
readonly scope: { readonly projects: readonly string[] }
|
|
43
|
+
readonly allow: readonly GrantVerb[]
|
|
44
|
+
readonly permissions: { readonly allow: readonly string[]; readonly never: readonly string[] }
|
|
45
|
+
readonly allowAlways: boolean
|
|
46
|
+
readonly maxActionsPerHour: number
|
|
47
|
+
readonly created: number
|
|
48
|
+
readonly expiresAt: number
|
|
49
|
+
readonly updated?: number
|
|
50
|
+
readonly revokedAt?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GrantView extends Grant {
|
|
54
|
+
readonly state: GrantState
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface GrantsDeps {
|
|
58
|
+
readonly state: StateStore
|
|
59
|
+
/** The hourly action counts (consume). */
|
|
60
|
+
readonly counters: Counters
|
|
61
|
+
readonly directory: Pick<Directory, "resolve">
|
|
62
|
+
readonly store: Pick<Store, "user" | "agentData">
|
|
63
|
+
/** The hub-wide admin audit (Store): summaries only. */
|
|
64
|
+
readonly admin?: { audit(entry: Omit<AuditEntry, "at">): void | Promise<void> }
|
|
65
|
+
readonly now?: () => number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const notFound = () => new AgentError("not_found", "No such grant")
|
|
69
|
+
|
|
70
|
+
export class Grants {
|
|
71
|
+
private readonly now: () => number
|
|
72
|
+
|
|
73
|
+
constructor(private readonly deps: GrantsDeps) {
|
|
74
|
+
this.now = deps.now ?? Date.now
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validates and stores a new grant of the owner (the holder resolved and pinned for the owner). */
|
|
78
|
+
async create(owner: string, input: unknown): Promise<Grant> {
|
|
79
|
+
this.owner(owner)
|
|
80
|
+
const spec = validateGrant(input)
|
|
81
|
+
const holderSessionID = this.pin(owner, spec.holder)
|
|
82
|
+
const all = await this.all(owner)
|
|
83
|
+
const at = this.now()
|
|
84
|
+
if (all.filter((g) => grantState(g, at) === "active").length >= GRANTS_ACTIVE_MAX) {
|
|
85
|
+
throw new AgentError("rate_limited", `At most ${GRANTS_ACTIVE_MAX} active grants per user`, { hint: "Revoke unused grants" })
|
|
86
|
+
}
|
|
87
|
+
const grant: Grant = { id: `g_${randomBytes(8).toString("hex")}`, owner, ...fields(spec, at), ...(holderSessionID ? { holderSessionID } : {}), created: at }
|
|
88
|
+
await this.deps.state.put(owner, COLLECTION, grant.id, grant)
|
|
89
|
+
await this.prune(owner, all, at)
|
|
90
|
+
await this.deps.admin?.audit({ actor: owner, action: "grant.create", target: grant.id })
|
|
91
|
+
return grant
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** One of the owner's grants (any state); not_found otherwise. */
|
|
95
|
+
async get(owner: string, id: string): Promise<Grant> {
|
|
96
|
+
if (!ID.test(id)) throw notFound()
|
|
97
|
+
const found = await this.deps.state.get<Grant>(owner, COLLECTION, id)
|
|
98
|
+
if (!found) throw notFound()
|
|
99
|
+
return found.value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The owner's grants (every state), newest first. */
|
|
103
|
+
async list(owner: string): Promise<GrantView[]> {
|
|
104
|
+
const at = this.now()
|
|
105
|
+
return (await this.all(owner)).sort((a, b) => b.created - a.created || (a.id < b.id ? -1 : 1)).map((g) => ({ ...g, state: grantState(g, at) }))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Replaces an active grant with a new spec (its expiry restarts now); id, holder (and its pin) and created
|
|
110
|
+
* stay. Changing the holder is invalid (revoke and create instead); an ended grant is gone.
|
|
111
|
+
*/
|
|
112
|
+
async update(owner: string, id: string, input: unknown): Promise<Grant> {
|
|
113
|
+
const spec = validateGrant(input)
|
|
114
|
+
return this.change(owner, id, "grant.update", (grant, at) => {
|
|
115
|
+
if (spec.holder !== grant.holder) throw new AgentError("invalid", "A grant's holder cannot change: revoke it and create another")
|
|
116
|
+
return { ...grant, ...fields(spec, at), updated: at }
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Ends an active grant now; an ended one is gone. */
|
|
121
|
+
async revoke(owner: string, id: string): Promise<Grant> {
|
|
122
|
+
return this.change(owner, id, "grant.revoke", (grant, at) => ({ ...grant, revokedAt: at }))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A version-checked change of an active grant. */
|
|
126
|
+
private async change(owner: string, id: string, action: string, apply: (grant: Grant, at: number) => Grant): Promise<Grant> {
|
|
127
|
+
await this.get(owner, id)
|
|
128
|
+
let missing = false
|
|
129
|
+
const next = await updateVersioned<Grant>(this.deps.state, owner, COLLECTION, id, (current) => {
|
|
130
|
+
if (!current) {
|
|
131
|
+
missing = true
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
const at = this.now()
|
|
135
|
+
const state = grantState(current, at)
|
|
136
|
+
if (state !== "active") throw new AgentError("gone", `The grant is ${state}`)
|
|
137
|
+
return apply(current, at)
|
|
138
|
+
})
|
|
139
|
+
if (missing || !next) throw notFound()
|
|
140
|
+
await this.deps.admin?.audit({ actor: owner, action, target: id })
|
|
141
|
+
return next
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The active grants the actor holds that allow `verb` on a session in `target.directory` (newest first),
|
|
146
|
+
* checked now: the owner is the actor's user and still enabled; a pinned holder is this session; a role
|
|
147
|
+
* holder's role is held by this session now; an app holder is this token and the owner still has it.
|
|
148
|
+
* Users act without grants (none). The caller still applies the policy, self-tree ban and kill switches.
|
|
149
|
+
*/
|
|
150
|
+
async usable(actor: AgentActor, verb: GrantVerb, target: { readonly directory: string }): Promise<Grant[]> {
|
|
151
|
+
if (actor.kind === "user") return []
|
|
152
|
+
const user = this.deps.store.user(actor.userID)
|
|
153
|
+
if (!user || user.disabled) return []
|
|
154
|
+
const at = this.now()
|
|
155
|
+
const out: Grant[] = []
|
|
156
|
+
for (const grant of await this.all(actor.userID)) {
|
|
157
|
+
if (grant.owner !== actor.userID || grantState(grant, at) !== "active" || !grant.allow.includes(verb)) continue
|
|
158
|
+
if (!inProjects(target.directory, grant.scope.projects) || !this.holds(grant, actor)) continue
|
|
159
|
+
out.push(grant)
|
|
160
|
+
}
|
|
161
|
+
return out.sort((a, b) => b.created - a.created)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Counts one action under the grant in the current clock hour; past maxActionsPerHour rate_limited (retryAfter). */
|
|
165
|
+
async consume(grant: Grant): Promise<void> {
|
|
166
|
+
const at = this.now()
|
|
167
|
+
const hour = Math.floor(at / HOUR_MS)
|
|
168
|
+
const count = await this.deps.counters.incr(`grant-actions:${grant.owner}:${grant.id}:${hour}`, HOUR_MS)
|
|
169
|
+
if (count > grant.maxActionsPerHour) {
|
|
170
|
+
const retryAfter = Math.max(1, Math.ceil(((hour + 1) * HOUR_MS - at) / 1000))
|
|
171
|
+
throw new AgentError("rate_limited", `The grant allows ${grant.maxActionsPerHour} actions an hour`, { extra: { retryAfter } })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The actor is the grant's holder now (same owner assumed). */
|
|
176
|
+
private holds(grant: Grant, actor: SessionActor | AppActor): boolean {
|
|
177
|
+
if (grant.holder.startsWith("app:")) {
|
|
178
|
+
if (actor.kind !== "app" || grant.holder !== `app:${actor.tokenID}`) return false
|
|
179
|
+
return (this.deps.store.agentData(grant.owner).appTokens ?? []).some((t) => t.id === actor.tokenID)
|
|
180
|
+
}
|
|
181
|
+
if (actor.kind !== "session") return false
|
|
182
|
+
if (grant.holderSessionID !== undefined) return grant.holderSessionID === actor.sessionID
|
|
183
|
+
if (!grant.holder.startsWith("@@")) return false
|
|
184
|
+
try {
|
|
185
|
+
return this.deps.directory.resolve(grant.owner, grant.holder).sessions.some((s) => s.sessionID === actor.sessionID)
|
|
186
|
+
} catch {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private owner(owner: string): void {
|
|
192
|
+
const user = this.deps.store.user(owner)
|
|
193
|
+
if (!user || user.disabled) throw new AgentError("not_found", "No such user")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The session a ses_/@name holder names for the owner (not_found when it is not theirs); undefined for roles and apps. */
|
|
197
|
+
private pin(owner: string, holder: string): string | undefined {
|
|
198
|
+
if (holder.startsWith("app:")) {
|
|
199
|
+
const id = holder.slice(4)
|
|
200
|
+
if (!(this.deps.store.agentData(owner).appTokens ?? []).some((t) => t.id === id)) throw new AgentError("not_found", `No app token ${id}`)
|
|
201
|
+
return undefined
|
|
202
|
+
}
|
|
203
|
+
if (holder.startsWith("@@")) return undefined
|
|
204
|
+
const { sessions } = this.deps.directory.resolve(owner, holder)
|
|
205
|
+
return sessions[0]!.sessionID
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async all(owner: string): Promise<Grant[]> {
|
|
209
|
+
return (await this.deps.state.list<Grant>(owner, COLLECTION)).map((e) => e.value)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Drops the oldest ended grants past GRANTS_KEPT. */
|
|
213
|
+
private async prune(owner: string, before: readonly Grant[], at: number): Promise<void> {
|
|
214
|
+
const ended = before.filter((g) => grantState(g, at) !== "active").sort((a, b) => endOf(a) - endOf(b))
|
|
215
|
+
for (const grant of ended.slice(0, Math.max(0, ended.length - GRANTS_KEPT))) await this.deps.state.delete(owner, COLLECTION, grant.id)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A grant's state at a time. */
|
|
220
|
+
export function grantState(grant: Grant, at: number): GrantState {
|
|
221
|
+
if (grant.revokedAt !== undefined) return "revoked"
|
|
222
|
+
return at >= grant.expiresAt ? "expired" : "active"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const endOf = (grant: Grant): number => grant.revokedAt ?? grant.expiresAt
|
|
226
|
+
|
|
227
|
+
const fields = (spec: GrantSpec, at: number) => ({
|
|
228
|
+
holder: spec.holder,
|
|
229
|
+
scope: { projects: [...spec.scope.projects] },
|
|
230
|
+
allow: [...spec.allow],
|
|
231
|
+
permissions: { allow: [...spec.permissions.allow], never: [...spec.permissions.never] },
|
|
232
|
+
allowAlways: spec.allowAlways,
|
|
233
|
+
maxActionsPerHour: spec.maxActionsPerHour,
|
|
234
|
+
expiresAt: at + spec.expiresInMs,
|
|
235
|
+
})
|