@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.
- package/LICENSE +21 -0
- package/README.md +1055 -0
- package/bin/agent-discovery.mjs +71 -0
- package/bin/agent-icon.mjs +77 -0
- package/bin/agent-models.mjs +77 -0
- package/bin/agent-type.mjs +51 -0
- package/bin/agentdev.mjs +657 -0
- package/bin/app-route-script.mjs +59 -0
- package/bin/app-runtime-contract.mjs +2 -0
- package/bin/appdev-remote.mjs +346 -0
- package/bin/appdev.mjs +4446 -0
- package/bin/apps.mjs +5512 -0
- package/bin/capability-calls.mjs +437 -0
- package/bin/capsule-data.mjs +260 -0
- package/bin/client.mjs +189 -0
- package/bin/commands.mjs +1194 -0
- package/bin/dev-capsules.mjs +1599 -0
- package/bin/dev-contract.mjs +262 -0
- package/bin/dev-data.mjs +287 -0
- package/bin/dev-members.mjs +18 -0
- package/bin/dev-net.mjs +316 -0
- package/bin/dev-notification-popup.mjs +628 -0
- package/bin/dev-ports.mjs +567 -0
- package/bin/dev-server-binding.mjs +35 -0
- package/bin/dev-server-ops.mjs +1086 -0
- package/bin/dev-ui/IoskeleyMono-400.woff2 +0 -0
- package/bin/dev-ui/IoskeleyMono-600.woff2 +0 -0
- package/bin/dev-ui/OFL.txt +92 -0
- package/bin/dev-ui/agent-robot.webp +0 -0
- package/bin/dev-ui/app.js +5217 -0
- package/bin/dev-ui/highlight.js +195 -0
- package/bin/dev-ui/index.html +34 -0
- package/bin/dev-ui/style.css +3640 -0
- package/bin/devlint.mjs +112 -0
- package/bin/devserver.mjs +2127 -0
- package/bin/devtriggers.mjs +367 -0
- package/bin/endpoints.mjs +156 -0
- package/bin/errors.mjs +61 -0
- package/bin/files.mjs +169 -0
- package/bin/horizontal-capabilities/v1/contract.json +280 -0
- package/bin/http.mjs +500 -0
- package/bin/lint-manifests/justbash-commands.json +88 -0
- package/bin/lint-manifests/python-stdlib.json +295 -0
- package/bin/login-page.mjs +488 -0
- package/bin/schedules.mjs +664 -0
- package/bin/server-sandbox.mjs +204 -0
- package/bin/servicedev.mjs +425 -0
- package/bin/sync.mjs +357 -0
- package/bin/terminus.js +3666 -0
- package/bin/toolchain.mjs +125 -0
- package/bin/vendor/app-runtime-v1/app-host.json +124 -0
- package/bin/vendor/app-runtime-v1/capability-calls.json +412 -0
- package/bin/vendor/app-runtime-v1/doors.json +2867 -0
- package/bin/vendor/appd/node-harness.mjs +209 -0
- package/bin/vendor/appd/python-harness.py +12 -0
- package/bin/vendor/appd/server-protocol.json +84 -0
- package/bin/vendor/where.mjs +541 -0
- package/bin/versioning.mjs +72 -0
- package/bin/write-rules.mjs +398 -0
- package/package.json +41 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The appd node-lane boot shim (docs/app-server.md B6). The backend embeds
|
|
2
|
+
// this file verbatim (include_str), substitutes __ENTRYPOINT__, and sends it
|
|
3
|
+
// as the exec's `code`; bashd's agentOS lane runs it as an ES module from
|
|
4
|
+
// /tmp. It therefore never leans on the runtime's module resolver: stdlib
|
|
5
|
+
// arrives through ESM imports, and the developer's entry file is evaluated
|
|
6
|
+
// HERE, in a CommonJS wrapper whose require() serves a fixed stdlib table
|
|
7
|
+
// plus ./ siblings inside the server tree — so `exports.ops` works no matter
|
|
8
|
+
// what any package.json nearby says.
|
|
9
|
+
//
|
|
10
|
+
// Results ride a FILE, not stdout: bashd keeps only the first 64 KiB of
|
|
11
|
+
// stdout (head-truncated), so the op's return value is written to
|
|
12
|
+
// .terminus_result.json under the server base and read back out of the
|
|
13
|
+
// exec's `writes` diff. stdout belongs entirely to the app's own logging.
|
|
14
|
+
//
|
|
15
|
+
// TERMINUS_SERVER_BASE exists for the behavioral test suite
|
|
16
|
+
// (bashd/harness/harness.test.js); inside the isolate the env is denied, so
|
|
17
|
+
// the agentOS mount default always applies.
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import * as fsMod from "node:fs";
|
|
20
|
+
import * as pathMod from "node:path";
|
|
21
|
+
import * as cryptoMod from "node:crypto";
|
|
22
|
+
import * as utilMod from "node:util";
|
|
23
|
+
import * as bufferMod from "node:buffer";
|
|
24
|
+
|
|
25
|
+
const fs = fsMod;
|
|
26
|
+
const { Buffer } = bufferMod;
|
|
27
|
+
const posix = pathMod.posix || pathMod;
|
|
28
|
+
const SERVER_BASE =
|
|
29
|
+
(typeof process !== "undefined" && process.env && process.env.TERMINUS_SERVER_BASE) ||
|
|
30
|
+
"/home/agentos/server";
|
|
31
|
+
const RESULT_FILE = SERVER_BASE + "/.terminus_result.json";
|
|
32
|
+
const MAX_RESULT_BYTES = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
const inv = JSON.parse(fs.readFileSync(SERVER_BASE + "/.terminus_invocation.json", "utf8"));
|
|
35
|
+
|
|
36
|
+
// A failed syscall arrives on stderr as the binding host framed it
|
|
37
|
+
// (server-protocol.json `errors`): "<action> failed [<code> <status>]:
|
|
38
|
+
// <message>". The code and status become fields of the thrown Error, so ops
|
|
39
|
+
// branch on error.code, never on message text; the message keeps the
|
|
40
|
+
// platform's words. Anything unframed is the platform's own failure.
|
|
41
|
+
const FAILURE = /([a-z][a-z0-9_]*) failed \[([a-z][a-z0-9_]*) ([1-5][0-9][0-9])\]: ([\s\S]*)/;
|
|
42
|
+
|
|
43
|
+
function syscallError(binding, text) {
|
|
44
|
+
const raw = String(text || "").trim();
|
|
45
|
+
const framed = FAILURE.exec(raw);
|
|
46
|
+
const error = new Error(framed ? framed[1] + " failed: " + framed[4].trim() : raw || binding + " failed");
|
|
47
|
+
error.code = framed ? framed[2] : "internal_error";
|
|
48
|
+
error.status = framed ? Number(framed[3]) : 500;
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function call(binding, args) {
|
|
53
|
+
const spawned = spawnSync("agentos-terminus", [binding, ...args], {
|
|
54
|
+
maxBuffer: 7 * 1024 * 1024,
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
});
|
|
57
|
+
if (spawned.error) {
|
|
58
|
+
const error = syscallError(binding, binding + " failed: " + spawned.error.message);
|
|
59
|
+
error.cause = spawned.error;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const raw = String(spawned.stdout || "");
|
|
63
|
+
// Handler failures surface on stderr with a non-zero status, not in the
|
|
64
|
+
// ok-envelope.
|
|
65
|
+
if (spawned.status !== 0 || !raw.trim()) throw syscallError(binding, spawned.stderr);
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
if (!parsed.ok) throw syscallError(binding, parsed.error);
|
|
68
|
+
return parsed.result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const b64 = (value) => Buffer.from(value, "utf8").toString("base64");
|
|
72
|
+
|
|
73
|
+
// Syscall shapes are pinned by bashd/server-protocol.json `returns`:
|
|
74
|
+
// put -> {version}, delete -> {deleted}, get -> {found, doc: DATA, version},
|
|
75
|
+
// query -> {documents: [{doc: ID, data, version}], next_cursor}. Passed
|
|
76
|
+
// through unshaped.
|
|
77
|
+
const terminus = {
|
|
78
|
+
records: {
|
|
79
|
+
get: (scope, collection, doc) =>
|
|
80
|
+
call("records-get", ["--scope", scope, "--collection", collection, "--doc", doc]),
|
|
81
|
+
put: (scope, collection, doc, data, version) =>
|
|
82
|
+
call("records-put", [
|
|
83
|
+
"--scope", scope, "--collection", collection, "--doc", doc,
|
|
84
|
+
"--data", JSON.stringify(data),
|
|
85
|
+
...(version == null ? [] : ["--version", String(version)]),
|
|
86
|
+
]),
|
|
87
|
+
delete: (scope, collection, doc, version) =>
|
|
88
|
+
call("records-delete", [
|
|
89
|
+
"--scope", scope, "--collection", collection, "--doc", doc,
|
|
90
|
+
...(version == null ? [] : ["--version", String(version)]),
|
|
91
|
+
]),
|
|
92
|
+
// One page: `sort` is a top-level field ("-field" descending), `after`
|
|
93
|
+
// the previous page's next_cursor, `limit` 1-100 (default 100).
|
|
94
|
+
query: (scope, collection, opts = {}) =>
|
|
95
|
+
call("records-query", [
|
|
96
|
+
"--scope", scope, "--collection", collection,
|
|
97
|
+
...(opts.where ? ["--where", JSON.stringify(opts.where)] : []),
|
|
98
|
+
...(opts.sort ? ["--sort", String(opts.sort)] : []),
|
|
99
|
+
...(opts.after ? ["--after", String(opts.after)] : []),
|
|
100
|
+
...(opts.limit == null ? [] : ["--limit", String(opts.limit)]),
|
|
101
|
+
]),
|
|
102
|
+
// Compare-and-set in one call (docs/app-server.md B6): read the record,
|
|
103
|
+
// ask fn for the next value, write it against the version read (absent =
|
|
104
|
+
// "must not exist"), and on version_conflict read again - at most
|
|
105
|
+
// 1 + retries tries. fn is synchronous like every syscall and gets the
|
|
106
|
+
// current value (null when absent); returning undefined writes nothing.
|
|
107
|
+
update: (scope, collection, doc, fn, { retries = 5 } = {}) => {
|
|
108
|
+
if (typeof fn !== "function") {
|
|
109
|
+
throw new TypeError("records.update needs a function from the current value to the next");
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
112
|
+
throw new TypeError("records.update retries must be a whole number of at least 0");
|
|
113
|
+
}
|
|
114
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
115
|
+
const current = terminus.records.get(scope, collection, doc);
|
|
116
|
+
const value = current.found ? current.doc : null;
|
|
117
|
+
const next = fn(value, { version: current.version });
|
|
118
|
+
if (next && typeof next.then === "function") {
|
|
119
|
+
throw new TypeError("records.update's fn must return the next value, not a promise");
|
|
120
|
+
}
|
|
121
|
+
if (next === undefined) return { written: false, doc: value, version: current.version };
|
|
122
|
+
if (next === null) {
|
|
123
|
+
throw new TypeError(
|
|
124
|
+
"records.update's fn returned null: return undefined to leave the record as it is, or call records.delete",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const written = terminus.records.put(
|
|
129
|
+
scope, collection, doc, next, current.found ? current.version : 0,
|
|
130
|
+
);
|
|
131
|
+
return { written: true, doc: next, version: written.version };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!error || error.code !== "version_conflict" || attempt >= retries) throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
capsule: {
|
|
139
|
+
read: (path) => {
|
|
140
|
+
const r = call("capsule-read", ["--path", path]);
|
|
141
|
+
return Buffer.from(r.content_b64, "base64");
|
|
142
|
+
},
|
|
143
|
+
readText: (path) => {
|
|
144
|
+
const r = call("capsule-read", ["--path", path]);
|
|
145
|
+
return Buffer.from(r.content_b64, "base64").toString("utf8");
|
|
146
|
+
},
|
|
147
|
+
write: (path, content) =>
|
|
148
|
+
call("capsule-write", [
|
|
149
|
+
"--path", path,
|
|
150
|
+
"--content", b64(typeof content === "string" ? content : Buffer.from(content).toString("utf8")),
|
|
151
|
+
]),
|
|
152
|
+
},
|
|
153
|
+
webFetch: (url) => call("web-fetch", ["--url", url]),
|
|
154
|
+
egress: (template, params = {}) =>
|
|
155
|
+
call("egress-invoke", ["--template", template, "--params", JSON.stringify(params)]),
|
|
156
|
+
collect: {
|
|
157
|
+
submit: (channel, payload) =>
|
|
158
|
+
call("collect-submit", ["--channel", channel, "--payload", JSON.stringify(payload)]),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const BUILTINS = {
|
|
163
|
+
fs: fsMod, "node:fs": fsMod,
|
|
164
|
+
path: pathMod, "node:path": pathMod,
|
|
165
|
+
crypto: cryptoMod, "node:crypto": cryptoMod,
|
|
166
|
+
util: utilMod, "node:util": utilMod,
|
|
167
|
+
buffer: bufferMod, "node:buffer": bufferMod,
|
|
168
|
+
};
|
|
169
|
+
const moduleCache = new Map();
|
|
170
|
+
function loadLocal(request, fromDir) {
|
|
171
|
+
if (Object.prototype.hasOwnProperty.call(BUILTINS, request)) return BUILTINS[request];
|
|
172
|
+
if (!request.startsWith("./") && !request.startsWith("../")) {
|
|
173
|
+
throw new Error("require('" + request + "') is not available - stdlib and server/ siblings only");
|
|
174
|
+
}
|
|
175
|
+
let resolved = posix.normalize(posix.join(fromDir, request));
|
|
176
|
+
if (!resolved.startsWith(SERVER_BASE + "/")) {
|
|
177
|
+
throw new Error("require escapes server/: " + request);
|
|
178
|
+
}
|
|
179
|
+
if (!resolved.endsWith(".js") && !resolved.endsWith(".cjs") && !resolved.endsWith(".mjs")) {
|
|
180
|
+
resolved += ".js";
|
|
181
|
+
}
|
|
182
|
+
const cached = moduleCache.get(resolved);
|
|
183
|
+
if (cached) return cached.exports;
|
|
184
|
+
const source = fs.readFileSync(resolved, "utf8");
|
|
185
|
+
const localModule = { exports: {} };
|
|
186
|
+
moduleCache.set(resolved, localModule);
|
|
187
|
+
const dir = posix.dirname(resolved);
|
|
188
|
+
const requireFrom = (next) => loadLocal(next, dir);
|
|
189
|
+
const wrapped = new Function("module", "exports", "require", "__dirname", "__filename", source);
|
|
190
|
+
wrapped(localModule, localModule.exports, requireFrom, dir, resolved);
|
|
191
|
+
return localModule.exports;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const entry = loadLocal("./__ENTRYPOINT__", SERVER_BASE);
|
|
195
|
+
(async () => {
|
|
196
|
+
const handler = (entry.ops || {})[inv.op];
|
|
197
|
+
if (typeof handler !== "function") throw new Error("no server op '" + inv.op + "'");
|
|
198
|
+
const result = await handler(inv.input, { terminus, ctx: inv.ctx });
|
|
199
|
+
const serialized = JSON.stringify(result === undefined ? null : result);
|
|
200
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_RESULT_BYTES) {
|
|
201
|
+
throw new Error("op result exceeds the " + MAX_RESULT_BYTES + "-byte budget");
|
|
202
|
+
}
|
|
203
|
+
// Written LAST, after the handler returns, so app code can never leave a
|
|
204
|
+
// spoofed result behind: the harness's write wins.
|
|
205
|
+
fs.writeFileSync(RESULT_FILE, serialized);
|
|
206
|
+
})().catch((error) => {
|
|
207
|
+
console.error((error && error.stack) || String(error));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The appd python-lane shim (docs/app-server.md B6). Python server ops run as
|
|
2
|
+
# pure compute in v1 (no broker bindings): the shim hands the entry the op,
|
|
3
|
+
# input, and platform ctx via argv and runs it as __main__. The entry may
|
|
4
|
+
# write its result to .terminus_result.json under the server base; the
|
|
5
|
+
# backend reads it out of the exec's writes diff. The just-bash lane mounts
|
|
6
|
+
# the tree at /server; TERMINUS_SERVER_BASE exists for the test suite only.
|
|
7
|
+
import json, os, runpy, sys
|
|
8
|
+
|
|
9
|
+
base = os.environ.get("TERMINUS_SERVER_BASE", "/server")
|
|
10
|
+
inv = json.load(open(base + "/.terminus_invocation.json"))
|
|
11
|
+
sys.argv = ["op", inv["op"], json.dumps(inv["input"]), json.dumps(inv.get("ctx", {}))]
|
|
12
|
+
runpy.run_path(base + "/__ENTRYPOINT__", run_name="__main__")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comment": "Machine-readable half of docs/app-server.md. bashd/protocol.test.js, bashd/bindings.test.js, bashd/harness/harness.test.js and src/routes/app_server/{protocol.rs,tests.rs} lockstep-test their implementations against this file. Change the protocol here first.",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"audience": "terminus-appd-exec",
|
|
5
|
+
"invocation_path": "server/.terminus_invocation.json",
|
|
6
|
+
"result_path": "server/.terminus_result.json",
|
|
7
|
+
"exec": {
|
|
8
|
+
"files_field": "content_b64",
|
|
9
|
+
"code_field": "code",
|
|
10
|
+
"cwd": "server",
|
|
11
|
+
"mount_base": "/home/agentos/server",
|
|
12
|
+
"diff_prefix_node": "home/"
|
|
13
|
+
},
|
|
14
|
+
"errors": {
|
|
15
|
+
"channel": "stderr",
|
|
16
|
+
"exit_status": 1,
|
|
17
|
+
"format": "<action> failed [<code> <status>]: <message>",
|
|
18
|
+
"example": "records_put failed [version_conflict 409]: the record already exists (if_version 0 requires absence)",
|
|
19
|
+
"thrown": ["code", "status"],
|
|
20
|
+
"untagged": { "code": "internal_error", "status": 500 },
|
|
21
|
+
"cas_retry_code": "version_conflict",
|
|
22
|
+
"conflict_substrings": ["reread and retry", "requires absence"]
|
|
23
|
+
},
|
|
24
|
+
"returns": {
|
|
25
|
+
"records_put": ["version"],
|
|
26
|
+
"records_delete": ["deleted"],
|
|
27
|
+
"records_get": ["found", "doc", "version"],
|
|
28
|
+
"records_query": ["documents", "next_cursor"],
|
|
29
|
+
"collect_submit": ["id", "delivered"]
|
|
30
|
+
},
|
|
31
|
+
"helpers": {
|
|
32
|
+
"records.update": { "retries": 5, "retry_on": "version_conflict", "returns": ["written", "doc", "version"] }
|
|
33
|
+
},
|
|
34
|
+
"broker_capabilities": ["web_fetch", "load_file", "records", "capsule", "egress", "collect"],
|
|
35
|
+
"server_capabilities": ["records", "capsule", "web_fetch", "egress", "collect"],
|
|
36
|
+
"scopes": {
|
|
37
|
+
"records": "appd:records",
|
|
38
|
+
"capsule": "appd:capsule",
|
|
39
|
+
"web_fetch": "appd:web_fetch",
|
|
40
|
+
"egress": "appd:egress",
|
|
41
|
+
"collect": "appd:collect",
|
|
42
|
+
"installation_prefix": "inst:"
|
|
43
|
+
},
|
|
44
|
+
"bindings": {
|
|
45
|
+
"records-get": "records_get",
|
|
46
|
+
"records-put": "records_put",
|
|
47
|
+
"records-delete": "records_delete",
|
|
48
|
+
"records-query": "records_query",
|
|
49
|
+
"capsule-read": "capsule_read",
|
|
50
|
+
"capsule-write": "capsule_write",
|
|
51
|
+
"web-fetch": "web_fetch",
|
|
52
|
+
"load-file": "load_file",
|
|
53
|
+
"egress-invoke": "egress_invoke",
|
|
54
|
+
"collect-submit": "collect_submit"
|
|
55
|
+
},
|
|
56
|
+
"actions": {
|
|
57
|
+
"records_get": { "request": ["scope", "collection", "doc_id"], "response": ["found", "doc", "version"] },
|
|
58
|
+
"records_put": { "request": ["scope", "collection", "doc_id", "data", "if_version?"], "response": ["version"] },
|
|
59
|
+
"records_delete": { "request": ["scope", "collection", "doc_id", "if_version?"], "response": ["deleted"] },
|
|
60
|
+
"records_query": { "request": ["scope", "collection", "where?", "sort?", "after?", "limit?"], "response": ["documents", "next_cursor"] },
|
|
61
|
+
"capsule_read": { "request": ["path"], "response": ["path", "size_bytes", "content_b64"] },
|
|
62
|
+
"capsule_write": { "request": ["path", "content_b64"], "response": ["path", "size_bytes"] },
|
|
63
|
+
"load_file": { "request": ["path"], "response": ["content_b64", "size_bytes", "content_sha256"] },
|
|
64
|
+
"web_fetch": { "request": ["url"], "response": ["status", "url", "content_type", "body", "truncated"] },
|
|
65
|
+
"egress_invoke": { "request": ["template", "params"], "response": ["status", "content_type", "body", "body_b64", "truncated"] },
|
|
66
|
+
"collect_submit": { "request": ["channel", "payload"], "response": ["id", "delivered"] }
|
|
67
|
+
},
|
|
68
|
+
"limits": {
|
|
69
|
+
"broker_calls_per_minute": 600,
|
|
70
|
+
"op_result_bytes": 1048576,
|
|
71
|
+
"web_fetches_per_installation_per_day": 2000,
|
|
72
|
+
"record_doc_bytes": 262144,
|
|
73
|
+
"records_global_quota_bytes": 67108864,
|
|
74
|
+
"records_installation_quota_bytes": 1048576,
|
|
75
|
+
"records_query_limit": 100,
|
|
76
|
+
"records_query_page_bytes": 4194304,
|
|
77
|
+
"records_sort_value_bytes": 1024,
|
|
78
|
+
"capsule_file_bytes": 1048576,
|
|
79
|
+
"collect_payload_bytes": 65536,
|
|
80
|
+
"collect_submissions_per_installation_per_day": 100,
|
|
81
|
+
"sync_op_timeout_ms": 10000,
|
|
82
|
+
"max_op_timeout_ms": 120000
|
|
83
|
+
}
|
|
84
|
+
}
|