@terminus-ai/cli 0.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -0
  3. package/bin/agent-discovery.mjs +71 -0
  4. package/bin/agent-icon.mjs +77 -0
  5. package/bin/agent-models.mjs +77 -0
  6. package/bin/agent-type.mjs +51 -0
  7. package/bin/agentdev.mjs +657 -0
  8. package/bin/app-route-script.mjs +59 -0
  9. package/bin/app-runtime-contract.mjs +2 -0
  10. package/bin/appdev-remote.mjs +346 -0
  11. package/bin/appdev.mjs +4446 -0
  12. package/bin/apps.mjs +5512 -0
  13. package/bin/capability-calls.mjs +437 -0
  14. package/bin/capsule-data.mjs +260 -0
  15. package/bin/client.mjs +189 -0
  16. package/bin/commands.mjs +1194 -0
  17. package/bin/dev-capsules.mjs +1599 -0
  18. package/bin/dev-contract.mjs +262 -0
  19. package/bin/dev-data.mjs +287 -0
  20. package/bin/dev-members.mjs +18 -0
  21. package/bin/dev-net.mjs +316 -0
  22. package/bin/dev-notification-popup.mjs +628 -0
  23. package/bin/dev-ports.mjs +567 -0
  24. package/bin/dev-server-binding.mjs +35 -0
  25. package/bin/dev-server-ops.mjs +1086 -0
  26. package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
  27. package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
  28. package/bin/dev-ui/OFL.txt +92 -0
  29. package/bin/dev-ui/agent-robot.webp +0 -0
  30. package/bin/dev-ui/app.js +5217 -0
  31. package/bin/dev-ui/highlight.js +195 -0
  32. package/bin/dev-ui/index.html +34 -0
  33. package/bin/dev-ui/style.css +3640 -0
  34. package/bin/devlint.mjs +112 -0
  35. package/bin/devserver.mjs +2127 -0
  36. package/bin/devtriggers.mjs +367 -0
  37. package/bin/endpoints.mjs +156 -0
  38. package/bin/errors.mjs +61 -0
  39. package/bin/files.mjs +169 -0
  40. package/bin/horizontal-capabilities/v1/contract.json +280 -0
  41. package/bin/http.mjs +500 -0
  42. package/bin/lint-manifests/justbash-commands.json +88 -0
  43. package/bin/lint-manifests/python-stdlib.json +295 -0
  44. package/bin/login-page.mjs +488 -0
  45. package/bin/schedules.mjs +664 -0
  46. package/bin/server-sandbox.mjs +204 -0
  47. package/bin/servicedev.mjs +425 -0
  48. package/bin/sync.mjs +357 -0
  49. package/bin/terminus.js +3666 -0
  50. package/bin/toolchain.mjs +125 -0
  51. package/bin/vendor/app-runtime-v1/app-host.json +124 -0
  52. package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
  53. package/bin/vendor/app-runtime-v1/doors.json +2867 -0
  54. package/bin/vendor/appd/node-harness.mjs +209 -0
  55. package/bin/vendor/appd/python-harness.py +12 -0
  56. package/bin/vendor/appd/server-protocol.json +84 -0
  57. package/bin/vendor/where.mjs +541 -0
  58. package/bin/versioning.mjs +72 -0
  59. package/bin/write-rules.mjs +398 -0
  60. package/package.json +41 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Toolchain resolution for local agent dev: find the `terminus-agentd`
3
+ * binary `terminus dev` runs on.
4
+ *
5
+ * Resolution order: `--agentd <path>` / `$TERMINUS_AGENTD` → a
6
+ * `terminus-agentd` already on PATH → the `@terminus-ai/agentd-<platform>`
7
+ * package npm installed alongside this CLI.
8
+ *
9
+ * The npm package is the lane that works for everyone: `os`/`cpu` fields
10
+ * mean npm installs exactly the one matching build, so the binary is simply
11
+ * on disk before the dev ever starts.
12
+ */
13
+
14
+ import { constants as fsConstants } from "node:fs";
15
+ import { access, chmod, stat } from "node:fs/promises";
16
+ import { createRequire } from "node:module";
17
+ import path from "node:path";
18
+
19
+ import { CliError } from "./client.mjs";
20
+
21
+ /**
22
+ * Per-platform binary packages, declared as optionalDependencies so npm
23
+ * installs the single one this machine can run. Built and published from
24
+ * terminus-agent's `release-binaries` workflow; the table there
25
+ * (`npm/stage.mjs`) is the other half of this one and must agree with it.
26
+ */
27
+ export const AGENTD_PACKAGES = {
28
+ "darwin-arm64": "@terminus-ai/agentd-darwin-arm64",
29
+ "darwin-x64": "@terminus-ai/agentd-darwin-x64",
30
+ "linux-x64": "@terminus-ai/agentd-linux-x64",
31
+ "linux-arm64": "@terminus-ai/agentd-linux-arm64",
32
+ };
33
+
34
+ export function agentdPackage(platform = process.platform, arch = process.arch) {
35
+ return AGENTD_PACKAGES[`${platform}-${arch}`] ?? null;
36
+ }
37
+
38
+ /** The Rust target each package's binary is built for — terminus-agent's
39
+ * release matrix, which the package table above follows. */
40
+ export function agentdTriple(platform = process.platform, arch = process.arch) {
41
+ const triples = {
42
+ "darwin-arm64": "aarch64-apple-darwin",
43
+ "darwin-x64": "x86_64-apple-darwin",
44
+ "linux-x64": "x86_64-unknown-linux-musl",
45
+ "linux-arm64": "aarch64-unknown-linux-musl",
46
+ };
47
+ return triples[`${platform}-${arch}`] ?? null;
48
+ }
49
+
50
+ async function executable(candidate) {
51
+ try {
52
+ await access(candidate, fsConstants.X_OK);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ async function onPath(name) {
60
+ const separator = process.platform === "win32" ? ";" : ":";
61
+ for (const dir of (process.env.PATH ?? "").split(separator)) {
62
+ if (!dir) continue;
63
+ const candidate = path.join(dir, name);
64
+ if (await executable(candidate)) return candidate;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * The binary inside the installed per-platform package, if npm put one
71
+ * there. Resolves `package.json` rather than the binary itself so that a
72
+ * package that is present but unstaged — the `npm link` case during local
73
+ * development — is distinguishable from one that was never installed;
74
+ * `resolveAgentd` turns that into an error that says what to run.
75
+ */
76
+ async function packagedAgentd() {
77
+ const name = agentdPackage();
78
+ if (!name) return { name: null, dir: null, binary: null };
79
+ let dir = null;
80
+ try {
81
+ dir = path.dirname(createRequire(import.meta.url).resolve(`${name}/package.json`));
82
+ } catch {
83
+ return { name, dir: null, binary: null };
84
+ }
85
+ const binary = path.join(dir, "bin", "terminus-agentd");
86
+ if (await executable(binary)) return { name, dir, binary };
87
+ // Present but not executable: a packaging step that dropped the mode bit,
88
+ // or a staged copy written without one. Cheap to repair, confusing to hit.
89
+ const exists = await stat(binary).then(() => true, () => false);
90
+ if (exists && (await chmod(binary, 0o755).then(() => true, () => false))) {
91
+ if (await executable(binary)) return { name, dir, binary };
92
+ }
93
+ return { name, dir, binary: null };
94
+ }
95
+
96
+ /** Resolve the terminus-agentd binary the dev should spawn. */
97
+ export async function resolveAgentd(flags = {}) {
98
+ const explicit = flags.agentd ?? process.env.TERMINUS_AGENTD;
99
+ if (explicit) {
100
+ if (await executable(explicit)) return explicit;
101
+ throw new CliError(`--agentd/${"$"}TERMINUS_AGENTD points at ${explicit}, which is not executable`);
102
+ }
103
+ // PATH stays ahead of the package: it is how someone building agentd
104
+ // itself runs their own build.
105
+ const found = await onPath("terminus-agentd");
106
+ if (found) return found;
107
+ const packaged = await packagedAgentd();
108
+ if (packaged.binary) return packaged.binary;
109
+ if (packaged.dir) {
110
+ // Linked or vendored, but carrying no binary — only reachable during
111
+ // local development, where the fix is one command.
112
+ throw new CliError(
113
+ `${packaged.name} is installed at ${packaged.dir} but holds no binary — `
114
+ + "stage one with `node npm/stage.mjs --from target/release/terminus-agentd` "
115
+ + "in the terminus-agent repo, or point $TERMINUS_AGENTD at a build",
116
+ );
117
+ }
118
+ throw new CliError(
119
+ "the local dev needs the terminus-agentd binary, and "
120
+ + `${packaged.name ? `${packaged.name} is not installed` : `there is no build for ${process.platform}/${process.arch}`} — `
121
+ + "install terminus-agentd on your PATH (cargo build --release -p "
122
+ + "terminus-agentd in the terminus-agent repo), or point $TERMINUS_AGENTD "
123
+ + "/ --agentd at one",
124
+ );
125
+ }
@@ -0,0 +1,124 @@
1
+ {
2
+ "schema_version": 1,
3
+ "contract": "terminus.app-runtime.app-host",
4
+ "runtime_api_version": 1,
5
+ "prefix": "/_terminus",
6
+ "routing": [
7
+ "worker_handled paths are answered by the app host itself.",
8
+ "bootstrap answers only GET and HEAD (405 method_not_allowed otherwise) and is rewritten to its upstream.",
9
+ "Without a session cookie, an app open to guests answers guest.bootstrap itself, forwards guest.doors (a capability asset only for a capability its current release declares) and refuses every other door with guest.refusal. A top-level page navigation from a browser without guest.marker_cookie goes once to the platform's authorization page first: a person signed in there comes back with a session, anyone else comes back through /_terminus/guest, which sets the marker. Any other app answers 401 unauthorized to a fetch and redirects a navigation to the platform's authorization page.",
10
+ "The suffix after the prefix is at most 1024 characters; each percent-decoded segment is non-empty, not . or .., and free of / \\ and control characters; the first segment must be in allowed_segments. Anything else is 404 not_found.",
11
+ "A request other than GET, HEAD or OPTIONS must carry an Origin (or else a Referer) equal to the app's own origin, or it is 403 forbidden.",
12
+ "rewrites apply by path prefix (their request_headers are set on the upstream call); every other door maps to default_upstream_prefix plus the suffix. The query string is preserved, the method and body pass through, the session rides as Authorization: Bearer, and redirects are passed through rather than followed."
13
+ ],
14
+ "worker_handled": ["icon", "logout", "callback", "open", "signin", "guest"],
15
+ "bootstrap": {
16
+ "path": "/bootstrap",
17
+ "methods": ["GET", "HEAD"],
18
+ "upstream": "/v1/app-runtime/session/current"
19
+ },
20
+ "allowed_segments": [
21
+ "capabilities",
22
+ "collaboration",
23
+ "collect",
24
+ "collections",
25
+ "connectors",
26
+ "data",
27
+ "egress",
28
+ "events",
29
+ "jobs",
30
+ "lifecycle",
31
+ "logs",
32
+ "net",
33
+ "notifications",
34
+ "schedules",
35
+ "server",
36
+ "service-jobs",
37
+ "services",
38
+ "spaces",
39
+ "storage",
40
+ "transactions",
41
+ "users"
42
+ ],
43
+ "rewrites": [
44
+ {
45
+ "prefix": "/capabilities",
46
+ "upstream_prefix": "/v1/capabilities",
47
+ "request_headers": { "x-terminus-artifact-context": "app" }
48
+ }
49
+ ],
50
+ "default_upstream_prefix": "/v1/app-runtime",
51
+ "request_headers": ["accept", "content-type", "idempotency-key", "if-none-match"],
52
+ "response_headers": [
53
+ "cache-control",
54
+ "content-disposition",
55
+ "content-language",
56
+ "content-type",
57
+ "etag",
58
+ "last-modified",
59
+ "location",
60
+ "vary",
61
+ "retry-after",
62
+ "x-terminus-data-version",
63
+ "x-terminus-schema-id",
64
+ "x-terminus-schema-version",
65
+ "x-terminus-observation-id",
66
+ "x-terminus-provenance",
67
+ "x-terminus-url"
68
+ ],
69
+ "added_response_headers": { "x-content-type-options": "nosniff" },
70
+ "response_header_overrides": [
71
+ { "prefix": "/service-jobs/", "headers": { "cache-control": "private, no-store" } }
72
+ ],
73
+ "asset_identity": "/v1/app-runtime/session/identity",
74
+ "session_cookie": "__Host-terminus_app",
75
+ "guest": {
76
+ "bootstrap": {
77
+ "platform_api_version": 1,
78
+ "guest": true,
79
+ "app": {
80
+ "id": "{resolve.app_id}",
81
+ "slug": "{resolve.slug}",
82
+ "name": "{resolve.name}",
83
+ "icon_url": "/_terminus/icon"
84
+ },
85
+ "installation": null,
86
+ "user": null,
87
+ "data": { "grants": [], "resource_grants": [] }
88
+ },
89
+ "doors": ["capabilities.asset"],
90
+ "marker_cookie": "__Host-terminus_guest",
91
+ "marker_max_age_seconds": 43200,
92
+ "refusal": { "status": 401, "code": "unauthorized" }
93
+ },
94
+ "errors": [
95
+ {
96
+ "when": "an unknown or unsafe /_terminus path, or a first segment outside allowed_segments",
97
+ "status": 404,
98
+ "code": "not_found"
99
+ },
100
+ {
101
+ "when": "bootstrap with a method other than GET or HEAD, or a worker-handled door with a method other than its own",
102
+ "status": 405,
103
+ "code": "method_not_allowed"
104
+ },
105
+ { "when": "a cross-origin mutation", "status": 403, "code": "forbidden" },
106
+ {
107
+ "when": "a door without a session (a signed-out fetch, or a guest calling a door outside guest.doors)",
108
+ "status": 401,
109
+ "code": "unauthorized"
110
+ },
111
+ { "when": "the backend cannot be reached", "status": 503, "code": "service_unavailable" }
112
+ ],
113
+ "html_pages": "Top-level navigations (an unknown app host, an unpublished app, a failed sign-in callback) render an HTML error page; every /_terminus answer that is not a navigation is the JSON error envelope.",
114
+ "worker_upstream": [
115
+ "GET /v1/app-runtime/resolve",
116
+ "GET /v1/app-runtime/icon",
117
+ "POST /v1/app-runtime/session",
118
+ "GET /v1/app-runtime/session/identity",
119
+ "DELETE /v1/app-runtime/session/current",
120
+ "GET /v1/app-runtime/assets/{*path}",
121
+ "GET /v1/app-runtime/guest-assets/{release_id}/{*path}",
122
+ "GET /v1/capabilities/{id}/{major}/assets/{*path}"
123
+ ]
124
+ }
@@ -0,0 +1,412 @@
1
+ {
2
+ "schema_version": 1,
3
+ "contract": "terminus.sdk.capability-calls",
4
+ "sources": {
5
+ "extensions": [".cjs", ".cts", ".html", ".js", ".jsx", ".mjs", ".mts", ".svelte", ".ts", ".tsx", ".vue"],
6
+ "skip_directories": ["__tests__", "fixtures", "test", "tests"],
7
+ "skip_files": "\\.(?:spec|test)\\.[^.]+$",
8
+ "comments": "Comments are masked before matching; string literals and offsets are preserved.",
9
+ "binding": "A call is <binding>.<method>( with an optional TypeScript type argument, where <binding> is the call's surface or a local alias from import { <surface> as <alias> } from \"@terminus-ai/app-sdk\"; a longer member path ending in the binding matches too (TerminusSDK.net.fetch(...)).",
10
+ "literals": "A literal argument is a single- or double-quoted string (never a template literal) or an integer literal at its listed index. A call to a compiled method whose literal arguments are not literal fails the build.",
11
+ "placeholders": "{name} in a grant is replaced by the literal argument of that name; integer arguments substitute as numbers."
12
+ },
13
+ "calls": [
14
+ {
15
+ "call": "horizontal.require",
16
+ "literal_args": [
17
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
18
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" }
19
+ ],
20
+ "grant": {
21
+ "capability": "horizontal",
22
+ "merge_by": "id",
23
+ "entry": { "id": "{id}", "version": "{version}", "operations": [] }
24
+ },
25
+ "checks": [
26
+ "{id} v{version} names a capability in the horizontal registry",
27
+ "a package uses one version of {id}"
28
+ ]
29
+ },
30
+ {
31
+ "call": "horizontal.loadModule",
32
+ "literal_args": [
33
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
34
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" }
35
+ ],
36
+ "grant": {
37
+ "capability": "horizontal",
38
+ "merge_by": "id",
39
+ "entry": { "id": "{id}", "version": "{version}", "operations": [] }
40
+ },
41
+ "checks": [
42
+ "{id} v{version} names a capability in the horizontal registry",
43
+ "a package uses one version of {id}"
44
+ ]
45
+ },
46
+ {
47
+ "call": "horizontal.loadStyle",
48
+ "literal_args": [
49
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
50
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" }
51
+ ],
52
+ "grant": {
53
+ "capability": "horizontal",
54
+ "merge_by": "id",
55
+ "entry": { "id": "{id}", "version": "{version}", "operations": [] }
56
+ },
57
+ "checks": [
58
+ "{id} v{version} names a capability in the horizontal registry",
59
+ "a package uses one version of {id}"
60
+ ]
61
+ },
62
+ {
63
+ "call": "horizontal.describe",
64
+ "literal_args": [
65
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
66
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" }
67
+ ],
68
+ "grant": {
69
+ "capability": "horizontal",
70
+ "merge_by": "id",
71
+ "entry": { "id": "{id}", "version": "{version}", "operations": [] }
72
+ },
73
+ "checks": [
74
+ "{id} v{version} names a capability in the horizontal registry",
75
+ "a package uses one version of {id}"
76
+ ]
77
+ },
78
+ {
79
+ "call": "horizontal.assetUrl",
80
+ "literal_args": [
81
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
82
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" }
83
+ ],
84
+ "grant": {
85
+ "capability": "horizontal",
86
+ "merge_by": "id",
87
+ "entry": { "id": "{id}", "version": "{version}", "operations": [] }
88
+ },
89
+ "checks": [
90
+ "{id} v{version} names a capability in the horizontal registry",
91
+ "a package uses one version of {id}"
92
+ ]
93
+ },
94
+ {
95
+ "call": "horizontal.moduleUrl",
96
+ "literal_args": [
97
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
98
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" }
99
+ ],
100
+ "grant": {
101
+ "capability": "horizontal",
102
+ "merge_by": "id",
103
+ "entry": { "id": "{id}", "version": "{version}", "operations": [] }
104
+ },
105
+ "checks": [
106
+ "{id} v{version} names a capability in the horizontal registry",
107
+ "a package uses one version of {id}"
108
+ ]
109
+ },
110
+ {
111
+ "call": "horizontal.invoke",
112
+ "literal_args": [
113
+ { "index": 0, "name": "id", "kind": "string", "pattern": "^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$" },
114
+ { "index": 1, "name": "version", "kind": "integer", "pattern": "^[1-9][0-9]*$" },
115
+ { "index": 2, "name": "operation", "kind": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$" }
116
+ ],
117
+ "grant": {
118
+ "capability": "horizontal",
119
+ "merge_by": "id",
120
+ "entry": { "id": "{id}", "version": "{version}", "operations": ["{operation}"] }
121
+ },
122
+ "checks": [
123
+ "{id} v{version} names a capability in the horizontal registry",
124
+ "a package uses one version of {id}"
125
+ ]
126
+ },
127
+ {
128
+ "call": "connectors.request",
129
+ "literal_args": [{ "index": 0, "name": "slug", "kind": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }],
130
+ "grant": { "capability": "connectors", "add": "{slug}" }
131
+ },
132
+ {
133
+ "call": "connectors.json",
134
+ "literal_args": [{ "index": 0, "name": "slug", "kind": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }],
135
+ "grant": { "capability": "connectors", "add": "{slug}" }
136
+ },
137
+ {
138
+ "call": "services.invoke",
139
+ "literal_args": [
140
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" },
141
+ { "index": 1, "name": "operation", "kind": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$" }
142
+ ],
143
+ "grant": {
144
+ "capability": "services",
145
+ "merge_by": "address",
146
+ "entry": { "address": "{address}", "operations": ["{operation}"] },
147
+ "also": {
148
+ "capability": "artifacts",
149
+ "entry": {
150
+ "address": "{address}",
151
+ "kind": "service",
152
+ "relationship": "uses",
153
+ "operations": ["{operation}"]
154
+ }
155
+ }
156
+ }
157
+ },
158
+ {
159
+ "call": "services.json",
160
+ "literal_args": [
161
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" },
162
+ { "index": 1, "name": "operation", "kind": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$" }
163
+ ],
164
+ "grant": {
165
+ "capability": "services",
166
+ "merge_by": "address",
167
+ "entry": { "address": "{address}", "operations": ["{operation}"] },
168
+ "also": {
169
+ "capability": "artifacts",
170
+ "entry": {
171
+ "address": "{address}",
172
+ "kind": "service",
173
+ "relationship": "uses",
174
+ "operations": ["{operation}"]
175
+ }
176
+ }
177
+ }
178
+ },
179
+ {
180
+ "call": "services.search",
181
+ "literal_args": [
182
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" }
183
+ ],
184
+ "grant": {
185
+ "capability": "services",
186
+ "merge_by": "address",
187
+ "entry": { "address": "{address}", "operations": ["search"] },
188
+ "also": {
189
+ "capability": "artifacts",
190
+ "entry": {
191
+ "address": "{address}",
192
+ "kind": "service",
193
+ "relationship": "uses",
194
+ "operations": ["search"]
195
+ }
196
+ }
197
+ }
198
+ },
199
+ {
200
+ "call": "services.fetch",
201
+ "literal_args": [
202
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" }
203
+ ],
204
+ "grant": {
205
+ "capability": "services",
206
+ "merge_by": "address",
207
+ "entry": { "address": "{address}", "operations": ["fetch"] },
208
+ "also": {
209
+ "capability": "artifacts",
210
+ "entry": {
211
+ "address": "{address}",
212
+ "kind": "service",
213
+ "relationship": "uses",
214
+ "operations": ["fetch"]
215
+ }
216
+ }
217
+ }
218
+ },
219
+ {
220
+ "call": "services.generateImage",
221
+ "literal_args": [
222
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" }
223
+ ],
224
+ "grant": {
225
+ "capability": "services",
226
+ "merge_by": "address",
227
+ "entry": { "address": "{address}", "operations": ["generate", "edit"] },
228
+ "also": {
229
+ "capability": "artifacts",
230
+ "entry": {
231
+ "address": "{address}",
232
+ "kind": "service",
233
+ "relationship": "uses",
234
+ "operations": ["generate", "edit"]
235
+ }
236
+ }
237
+ }
238
+ },
239
+ {
240
+ "call": "services.convertDocument",
241
+ "literal_args": [
242
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" }
243
+ ],
244
+ "grant": {
245
+ "capability": "services",
246
+ "merge_by": "address",
247
+ "entry": { "address": "{address}", "operations": ["convert"] },
248
+ "also": {
249
+ "capability": "artifacts",
250
+ "entry": {
251
+ "address": "{address}",
252
+ "kind": "service",
253
+ "relationship": "uses",
254
+ "operations": ["convert"]
255
+ }
256
+ }
257
+ }
258
+ },
259
+ {
260
+ "call": "services.executeCode",
261
+ "literal_args": [
262
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" }
263
+ ],
264
+ "grant": {
265
+ "capability": "services",
266
+ "merge_by": "address",
267
+ "entry": { "address": "{address}", "operations": ["execute"] },
268
+ "also": {
269
+ "capability": "artifacts",
270
+ "entry": {
271
+ "address": "{address}",
272
+ "kind": "service",
273
+ "relationship": "uses",
274
+ "operations": ["execute"]
275
+ }
276
+ }
277
+ }
278
+ },
279
+ {
280
+ "call": "services.sendEmail",
281
+ "literal_args": [
282
+ { "index": 0, "name": "address", "kind": "string", "pattern": "^@[^\\s/@]+/[a-z0-9][a-z0-9-]*$" }
283
+ ],
284
+ "grant": {
285
+ "capability": "services",
286
+ "merge_by": "address",
287
+ "entry": { "address": "{address}", "operations": ["send"] },
288
+ "also": {
289
+ "capability": "artifacts",
290
+ "entry": {
291
+ "address": "{address}",
292
+ "kind": "service",
293
+ "relationship": "uses",
294
+ "operations": ["send"]
295
+ }
296
+ }
297
+ }
298
+ },
299
+ {
300
+ "call": "egress.request",
301
+ "literal_args": [
302
+ { "index": 0, "name": "template", "kind": "string", "pattern": "^[a-z][a-z0-9_-]{0,63}$" }
303
+ ],
304
+ "grant": {
305
+ "capability": "egress",
306
+ "requires_declared": "terminus.json egress.templates[].name == {template}"
307
+ },
308
+ "checks": [
309
+ "the template is declared in terminus.json egress (its URL, headers and secrets cannot be read off a call site); capabilities.egress is the declared templates"
310
+ ]
311
+ },
312
+ {
313
+ "call": "collect.submit",
314
+ "literal_args": [
315
+ { "index": 0, "name": "channel", "kind": "string", "pattern": "^[a-z][a-z0-9_-]{0,47}$" }
316
+ ],
317
+ "grant": { "capability": "collect.channels", "key": "{channel}", "entry": { "kind": "documents" } }
318
+ },
319
+ {
320
+ "call": "collect.retract",
321
+ "literal_args": [
322
+ { "index": 0, "name": "channel", "kind": "string", "pattern": "^[a-z][a-z0-9_-]{0,47}$" }
323
+ ],
324
+ "grant": { "capability": "collect.channels", "key": "{channel}", "entry": { "kind": "documents" } }
325
+ },
326
+ {
327
+ "call": "server.call",
328
+ "literal_args": [{ "index": 0, "name": "op", "kind": "string", "pattern": "^[a-z][a-z0-9_-]{0,47}$" }],
329
+ "grant": { "capability": "server.ops", "key": "{op}", "entry": {} },
330
+ "checks": [
331
+ "only calls outside server/ count",
332
+ "the package has server/main.js or server/main.py, and a Node entry exports ops.{op}",
333
+ "at most 16 ops"
334
+ ]
335
+ },
336
+ {
337
+ "call": "net.fetch",
338
+ "literal_args": [],
339
+ "grant": { "capability": "network_proxy", "add": "public-text" }
340
+ },
341
+ {
342
+ "call": "net.image",
343
+ "literal_args": [],
344
+ "grant": { "capability": "network_proxy", "add": "public-image" }
345
+ },
346
+ {
347
+ "call": "notifications.create",
348
+ "literal_args": [],
349
+ "grant": { "capability": "shell.notifications", "set": true }
350
+ }
351
+ ],
352
+ "workload_steps": [
353
+ {
354
+ "action": "connector.request",
355
+ "params": { "connector": "string" },
356
+ "grant": { "capability": "connectors", "add": "{connector}" }
357
+ },
358
+ {
359
+ "action": "service.invoke",
360
+ "params": { "address": "string", "operation": "string" },
361
+ "grant": {
362
+ "capability": "services",
363
+ "merge_by": "address",
364
+ "entry": { "address": "{address}", "operations": ["{operation}"] },
365
+ "also": {
366
+ "capability": "artifacts",
367
+ "entry": { "address": "{address}", "kind": "service", "relationship": "uses", "operations": ["{operation}"] }
368
+ }
369
+ }
370
+ },
371
+ {
372
+ "action": "notification.create",
373
+ "params": {},
374
+ "grant": { "capability": "shell.notifications", "set": true }
375
+ },
376
+ {
377
+ "action": "server.run",
378
+ "params": { "op": "string" },
379
+ "grant": {
380
+ "capability": "server.ops",
381
+ "key": "{op}",
382
+ "entry": { "background_only": true, "timeout_ms": 120000 },
383
+ "unless": "the app also calls server.call(\"{op}\"), which keeps the interactive entry {}"
384
+ }
385
+ }
386
+ ],
387
+ "server_calls": [
388
+ {
389
+ "call": "terminus.records.get|put|delete|query|update",
390
+ "in": "server/ JavaScript",
391
+ "literal_args": [
392
+ { "index": 0, "name": "scope", "kind": "string", "pattern": "^(?:global|installation)$" },
393
+ { "index": 1, "name": "collection", "kind": "string", "pattern": "^[a-z][a-z0-9_-]{0,47}$" }
394
+ ],
395
+ "grant": {
396
+ "capability": "server.records",
397
+ "add": { "collections": "{collection}", "scopes": "{scope}" }
398
+ },
399
+ "checks": ["at most 16 record collections"]
400
+ }
401
+ ],
402
+ "notes": [
403
+ "net is a top-level SDK namespace (net.fetch, net.image); the client.net.* spelling is gone.",
404
+ "net.fetch and net.image take runtime URLs: the grant is the fixed SSRF-guarded broker lane, not a destination.",
405
+ "connectors.json is connectors.request with the answer parsed, so it compiles the same connector grant.",
406
+ "services.jobs.*, horizontal.list and every other SDK call compile no grant.",
407
+ "notifications.create compiles shell.notifications = true into the release; terminus.json never declares it.",
408
+ "A notification.create automation step compiles the same shell.notifications = true, so an app that notifies only from an automation declares nothing either.",
409
+ "shell.* names a top-level release section, not a capabilities group.",
410
+ "Compiled grants belong to the release, never to the author's files: clone and pull never write them back into terminus.json."
411
+ ]
412
+ }