@factiii/runner 0.10.1 → 0.11.0
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/dist/cli.js +1049 -476
- package/index/index.js +10 -4
- package/package.json +2 -1
- package/plugin/.claude-plugin/marketplace.json +12 -0
- package/plugin/factiii/.claude-plugin/plugin.json +7 -0
- package/plugin/factiii/bin/factiii-secrets +208 -0
- package/plugin/factiii/bin/report-state.sh +43 -0
- package/plugin/factiii/hooks/hooks.json +59 -0
- package/plugin/factiii/skills/factiii-secrets/SKILL.md +82 -0
- package/plugin/factiii/skills/share-a-port/SKILL.md +41 -0
package/index/index.js
CHANGED
|
@@ -6915,9 +6915,9 @@ var require_dist = __commonJS({
|
|
|
6915
6915
|
}
|
|
6916
6916
|
});
|
|
6917
6917
|
|
|
6918
|
-
// ../../node_modules/content-type/index.js
|
|
6918
|
+
// ../../node_modules/@modelcontextprotocol/sdk/node_modules/content-type/index.js
|
|
6919
6919
|
var require_content_type = __commonJS({
|
|
6920
|
-
"../../node_modules/content-type/index.js"(exports2) {
|
|
6920
|
+
"../../node_modules/@modelcontextprotocol/sdk/node_modules/content-type/index.js"(exports2) {
|
|
6921
6921
|
"use strict";
|
|
6922
6922
|
var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g;
|
|
6923
6923
|
var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/;
|
|
@@ -92495,14 +92495,20 @@ function text(value) {
|
|
|
92495
92495
|
return { content: [{ type: "text", text: value }] };
|
|
92496
92496
|
}
|
|
92497
92497
|
function makeTool(t) {
|
|
92498
|
+
const args = t.args.strict();
|
|
92498
92499
|
return {
|
|
92499
92500
|
name: t.name,
|
|
92500
92501
|
description: t.description,
|
|
92501
92502
|
inputSchema: t.inputSchema,
|
|
92502
|
-
handle: (raw) => t.run(
|
|
92503
|
+
handle: (raw) => t.run(args.parse(raw))
|
|
92503
92504
|
};
|
|
92504
92505
|
}
|
|
92505
|
-
var objectSchema = (properties, required2) => ({
|
|
92506
|
+
var objectSchema = (properties, required2) => ({
|
|
92507
|
+
type: "object",
|
|
92508
|
+
properties,
|
|
92509
|
+
required: required2,
|
|
92510
|
+
additionalProperties: false
|
|
92511
|
+
});
|
|
92506
92512
|
function buildTools(store2, spaceSlug, fileBaseUrl) {
|
|
92507
92513
|
return [
|
|
92508
92514
|
makeTool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factiii/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Factiii Runner, run Board AI agents on a machine you control. Pairs with the Factiii web/mobile clients over WebRTC.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"keywords": [
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist/cli.js",
|
|
26
|
+
"plugin/",
|
|
26
27
|
"index/index.js",
|
|
27
28
|
"index/image-package.json",
|
|
28
29
|
"vendor/frpc-linux-amd64",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "factiii-runner",
|
|
3
|
+
"description": "Everything a factiii runner hands its CLIs: session status hooks and workspace skills.",
|
|
4
|
+
"owner": { "name": "Factiii" },
|
|
5
|
+
"plugins": [
|
|
6
|
+
{
|
|
7
|
+
"name": "factiii",
|
|
8
|
+
"source": "./factiii",
|
|
9
|
+
"description": "Board terminal integration for claude and codex."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "factiii",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Board terminal integration: reports what the agent is doing to the session's status dot, and ships the skills a factiii workspace provides.",
|
|
5
|
+
"author": { "name": "Factiii" },
|
|
6
|
+
"homepage": "https://factiii.com"
|
|
7
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* factiii-secrets - run a command with this space's stored secrets injected.
|
|
4
|
+
*
|
|
5
|
+
* A PROXY, never a carrier. This process asks the runner to run the command;
|
|
6
|
+
* the runner is what holds the decrypted values, execs the command, and sends
|
|
7
|
+
* back output already redacted against them. Nothing secret ever enters this
|
|
8
|
+
* process, so nothing secret is visible to whatever spawned it - which is the
|
|
9
|
+
* whole point, because what spawns it is an AI agent.
|
|
10
|
+
*
|
|
11
|
+
* Finding the runner doubles as the gate: this plugin installs globally, so
|
|
12
|
+
* outside a factiii space there is no socket and the command says so rather
|
|
13
|
+
* than hanging.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const net = require('net');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const SOCK_NAME = 'secrets.sock';
|
|
22
|
+
/** Denial is not a command failure: a distinct code lets a script tell "the
|
|
23
|
+
* owner said no" from "the command ran and failed". */
|
|
24
|
+
const EXIT_DENIED = 77;
|
|
25
|
+
/** Nothing ran at all - no socket, no runner, bad usage. */
|
|
26
|
+
const EXIT_UNAVAILABLE = 3;
|
|
27
|
+
|
|
28
|
+
function usage() {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
[
|
|
31
|
+
'usage:',
|
|
32
|
+
' factiii-secrets run -- <command...> run it with the declared secrets injected',
|
|
33
|
+
' factiii-secrets list print the declared key NAMES (never values)',
|
|
34
|
+
''
|
|
35
|
+
].join('\n')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `<spaceDir>/state/secrets.sock`. The env var is what the runner sets on
|
|
40
|
+
* every spawn; the walk-up covers a caller that changed the working
|
|
41
|
+
* directory to somewhere the runner did not choose. */
|
|
42
|
+
function findSocket() {
|
|
43
|
+
const fromEnv = process.env.FACTIII_SECRETS_SOCK;
|
|
44
|
+
if (fromEnv) return fromEnv;
|
|
45
|
+
let dir = process.cwd();
|
|
46
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
47
|
+
const candidate = path.join(dir, 'state', SOCK_NAME);
|
|
48
|
+
try {
|
|
49
|
+
if (fs.statSync(candidate).isSocket()) return candidate;
|
|
50
|
+
} catch {
|
|
51
|
+
// Not here; keep walking.
|
|
52
|
+
}
|
|
53
|
+
dir = path.dirname(dir);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rebuild one shell word.
|
|
60
|
+
*
|
|
61
|
+
* The runner runs the command through a login shell and shows it to the owner
|
|
62
|
+
* verbatim for authorization, so what we send has to mean exactly what the
|
|
63
|
+
* caller's argv meant. Single quotes make every character literal; the only
|
|
64
|
+
* thing that cannot appear inside them is a single quote, which is spliced.
|
|
65
|
+
* Words that need no quoting are left bare so the authorization card stays
|
|
66
|
+
* readable.
|
|
67
|
+
*/
|
|
68
|
+
function shellQuote(word) {
|
|
69
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
|
|
70
|
+
return `'${word.split("'").join(`'\\''`)}'`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One NDJSON request, its stream of replies handled by `onMessage`. */
|
|
74
|
+
function request(socketPath, payload, onMessage) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const conn = net.createConnection(socketPath);
|
|
77
|
+
let buffer = '';
|
|
78
|
+
let settled = false;
|
|
79
|
+
const finish = (code) => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
conn.end();
|
|
83
|
+
resolve(code);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
conn.on('connect', () => conn.write(`${JSON.stringify(payload)}\n`));
|
|
87
|
+
conn.on('data', (chunk) => {
|
|
88
|
+
buffer += chunk.toString('utf-8');
|
|
89
|
+
let newline = buffer.indexOf('\n');
|
|
90
|
+
while (newline !== -1) {
|
|
91
|
+
const line = buffer.slice(0, newline);
|
|
92
|
+
buffer = buffer.slice(newline + 1);
|
|
93
|
+
if (line.trim()) {
|
|
94
|
+
let message;
|
|
95
|
+
try {
|
|
96
|
+
message = JSON.parse(line);
|
|
97
|
+
} catch {
|
|
98
|
+
// A malformed frame is the runner's bug, not the caller's; there
|
|
99
|
+
// is nothing useful to do with half a message.
|
|
100
|
+
message = null;
|
|
101
|
+
}
|
|
102
|
+
if (message) {
|
|
103
|
+
const code = onMessage(message);
|
|
104
|
+
if (typeof code === 'number') finish(code);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
newline = buffer.indexOf('\n');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
conn.on('error', (err) => {
|
|
111
|
+
// The path can be known and still have nothing listening: the store is
|
|
112
|
+
// served only while a deploy run is in progress, and a creator session
|
|
113
|
+
// drafting a skill is the common case of asking outside one.
|
|
114
|
+
const why =
|
|
115
|
+
err.code === 'ENOENT' || err.code === 'ECONNREFUSED'
|
|
116
|
+
? 'the secrets store is not being served right now. It is available during a deploy run'
|
|
117
|
+
: `cannot reach the runner (${err.message})`;
|
|
118
|
+
process.stderr.write(`factiii-secrets: ${why}.\n`);
|
|
119
|
+
finish(EXIT_UNAVAILABLE);
|
|
120
|
+
});
|
|
121
|
+
// Closed with no verdict: the runner died, or the deploy run was
|
|
122
|
+
// cancelled mid-command. Either way nothing was reported as finished.
|
|
123
|
+
conn.on('close', () => finish(EXIT_UNAVAILABLE));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function main() {
|
|
128
|
+
const [op, ...rest] = process.argv.slice(2);
|
|
129
|
+
|
|
130
|
+
if (!op || op === '-h' || op === '--help') {
|
|
131
|
+
usage();
|
|
132
|
+
return op ? 0 : EXIT_UNAVAILABLE;
|
|
133
|
+
}
|
|
134
|
+
if (op !== 'run' && op !== 'list') {
|
|
135
|
+
process.stderr.write(`factiii-secrets: unknown command "${op}".\n`);
|
|
136
|
+
usage();
|
|
137
|
+
return EXIT_UNAVAILABLE;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const socketPath = findSocket();
|
|
141
|
+
if (!socketPath) {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
'factiii-secrets: no secrets store is reachable here. This command works inside a factiii space while a deploy run is active.\n'
|
|
144
|
+
);
|
|
145
|
+
return EXIT_UNAVAILABLE;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (op === 'list') {
|
|
149
|
+
return request(socketPath, { op: 'list' }, (message) => {
|
|
150
|
+
if (message.type === 'keys') {
|
|
151
|
+
process.stdout.write(`${message.keys.join('\n')}\n`);
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
if (message.type === 'error') {
|
|
155
|
+
process.stderr.write(`factiii-secrets: ${message.message}\n`);
|
|
156
|
+
return EXIT_UNAVAILABLE;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// `--` is optional but documented: it is what stops the caller's own flags
|
|
162
|
+
// (-e, --prod) from being read as ours.
|
|
163
|
+
const argv = rest[0] === '--' ? rest.slice(1) : rest;
|
|
164
|
+
if (!argv.length) {
|
|
165
|
+
process.stderr.write('factiii-secrets: run needs a command.\n');
|
|
166
|
+
usage();
|
|
167
|
+
return EXIT_UNAVAILABLE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const command = argv.map(shellQuote).join(' ');
|
|
171
|
+
return request(
|
|
172
|
+
socketPath,
|
|
173
|
+
{ op: 'run', command, cwd: process.cwd() },
|
|
174
|
+
(message) => {
|
|
175
|
+
if (message.type === 'status') {
|
|
176
|
+
// stderr, so a caller capturing stdout gets the command's output and
|
|
177
|
+
// nothing of ours.
|
|
178
|
+
process.stderr.write(`factiii-secrets: ${message.text}\n`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (message.type === 'output') {
|
|
182
|
+
process.stdout.write(message.chunk);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (message.type === 'denied') {
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
'factiii-secrets: the owner declined to run that command.\n'
|
|
188
|
+
);
|
|
189
|
+
return EXIT_DENIED;
|
|
190
|
+
}
|
|
191
|
+
if (message.type === 'error') {
|
|
192
|
+
process.stderr.write(`factiii-secrets: ${message.message}\n`);
|
|
193
|
+
return EXIT_UNAVAILABLE;
|
|
194
|
+
}
|
|
195
|
+
if (message.type === 'exit') return message.code;
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main().then(
|
|
201
|
+
(code) => {
|
|
202
|
+
process.exitCode = code;
|
|
203
|
+
},
|
|
204
|
+
(err) => {
|
|
205
|
+
process.stderr.write(`factiii-secrets: ${err && err.message}\n`);
|
|
206
|
+
process.exitCode = EXIT_UNAVAILABLE;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tell the runner what the agent is doing, so the board's status dot reflects
|
|
3
|
+
# it. Called from hooks.json on the events both CLIs share.
|
|
4
|
+
#
|
|
5
|
+
# Everything is derived, nothing is injected. The runner sets a tmux session's
|
|
6
|
+
# environment only when it CREATES the session (`new-session -A -e`), so a var
|
|
7
|
+
# added by a runner upgrade never reaches a session that is already running —
|
|
8
|
+
# which on any real host is most of them. TMUX_PANE is set by tmux itself at
|
|
9
|
+
# pane creation, so it is already there in every session ever started.
|
|
10
|
+
#
|
|
11
|
+
# It doubles as the gate. This plugin installs globally, so its hooks fire on
|
|
12
|
+
# every claude and codex run on this machine; outside a factiii workspace there
|
|
13
|
+
# is no pane and no space redis to find, and this exits having done nothing.
|
|
14
|
+
#
|
|
15
|
+
# Never fails the CLI. A hook that exits non-zero can block the turn it fired
|
|
16
|
+
# on, and a status dot is not worth interrupting someone's work for.
|
|
17
|
+
set -u
|
|
18
|
+
[ -n "${TMUX_PANE:-}" ] || exit 0
|
|
19
|
+
|
|
20
|
+
# The space's redis sits at <spaceDir>/state/redis.sock and the CLI runs inside
|
|
21
|
+
# that space's tree, so walk up for it. Bounded by reaching the root.
|
|
22
|
+
dir="$PWD"
|
|
23
|
+
sock=""
|
|
24
|
+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
25
|
+
if [ -S "$dir/state/redis.sock" ]; then
|
|
26
|
+
sock="$dir/state/redis.sock"
|
|
27
|
+
break
|
|
28
|
+
fi
|
|
29
|
+
dir="$(dirname "$dir")"
|
|
30
|
+
done
|
|
31
|
+
[ -n "$sock" ] || exit 0
|
|
32
|
+
|
|
33
|
+
state="${1:-idle}"
|
|
34
|
+
# The pane is the identity; the runner resolves it to a session. Keeping that
|
|
35
|
+
# mapping on the runner is what stops this script from having to know how tmux
|
|
36
|
+
# sessions are named.
|
|
37
|
+
#
|
|
38
|
+
# SET then PUBLISH: the publish drives live dots, the key lets a subscriber
|
|
39
|
+
# that starts mid-session read where things stand without waiting for a hook.
|
|
40
|
+
# -t 2 so a dead socket costs milliseconds rather than hanging the turn.
|
|
41
|
+
redis-cli -t 2 -s "$sock" set "agent-state:$TMUX_PANE" "$state" >/dev/null 2>&1
|
|
42
|
+
redis-cli -t 2 -s "$sock" publish agent-state "$TMUX_PANE $state" >/dev/null 2>&1
|
|
43
|
+
exit 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/report-state.sh\" working",
|
|
9
|
+
"timeout": 5
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"Stop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/report-state.sh\" idle",
|
|
20
|
+
"timeout": 5
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"PermissionRequest": [
|
|
26
|
+
{
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"type": "command",
|
|
30
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/report-state.sh\" blocked",
|
|
31
|
+
"timeout": 5
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"Notification": [
|
|
37
|
+
{
|
|
38
|
+
"hooks": [
|
|
39
|
+
{
|
|
40
|
+
"type": "command",
|
|
41
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/report-state.sh\" blocked",
|
|
42
|
+
"timeout": 5
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"SessionEnd": [
|
|
48
|
+
{
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/report-state.sh\" idle",
|
|
53
|
+
"timeout": 5
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: factiii-secrets
|
|
3
|
+
description: Run a command that needs this workspace's stored secrets, with factiii-secrets. TRIGGER when a command needs an API token, deploy credential, database URL or any other secret value, when writing or revising a deploy skill, or whenever an environment variable a step needs appears to be missing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# factiii-secrets
|
|
7
|
+
|
|
8
|
+
This machine keeps the workspace's secrets in an encrypted store the owner
|
|
9
|
+
manages. You never hold those values, and you never need to: one command runs
|
|
10
|
+
anything that needs them.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
factiii-secrets run -- <command...>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It is already on your PATH. Nothing needs installing, and nothing needs
|
|
17
|
+
setting up before you use it.
|
|
18
|
+
|
|
19
|
+
## What actually happens
|
|
20
|
+
|
|
21
|
+
You run the command. It blocks. The owner sees the exact command on their
|
|
22
|
+
board, with the keys it will receive, and enters their password to authorize
|
|
23
|
+
it. The runner then decrypts the store for that one command, runs it with the
|
|
24
|
+
values in its environment, and hands you back its output with every secret
|
|
25
|
+
value replaced by `[REDACTED]`. `factiii-secrets` exits with the command's own
|
|
26
|
+
exit code, so `&&`, `||` and `set -e` all behave normally.
|
|
27
|
+
|
|
28
|
+
The values reach the command and stop there. They are never in your
|
|
29
|
+
environment, never in your context, and never in anything you can read.
|
|
30
|
+
|
|
31
|
+
## Using it
|
|
32
|
+
|
|
33
|
+
Write the command exactly as you would without the wrapper, referencing
|
|
34
|
+
secrets as ordinary `$NAME` variables:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
factiii-secrets run -- vercel deploy --prod --token "$VERCEL_TOKEN"
|
|
38
|
+
factiii-secrets run -- psql "$DATABASE_URL" -f migrations/latest.sql
|
|
39
|
+
factiii-secrets run -- ./scripts/publish.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Quote the variables. They expand on the runner's side, once the values are
|
|
43
|
+
present - not in your shell, where they are empty.
|
|
44
|
+
|
|
45
|
+
Wrap **only** the commands that need a secret. Everything else - builds,
|
|
46
|
+
tests, git, file edits - runs normally, unwrapped, and stays fast because it
|
|
47
|
+
needs no authorization.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
factiii-secrets list
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
prints the key NAMES the workspace has declared. Never their values; there is
|
|
54
|
+
no command that prints a value, on purpose.
|
|
55
|
+
|
|
56
|
+
## Things that will waste your time
|
|
57
|
+
|
|
58
|
+
- **Do not check whether these variables are set.** They are unset in your
|
|
59
|
+
shell by design and will stay that way for the whole session. `env`,
|
|
60
|
+
`printenv`, `test -n "$TOKEN"` and echo-checks all tell you nothing.
|
|
61
|
+
- **Do not report them as missing or stop over them.** An unset declared
|
|
62
|
+
variable is the security model working. Route the command through
|
|
63
|
+
`factiii-secrets run` and it gets what it needs.
|
|
64
|
+
- **Do not ask the owner to export them,** write them to a `.env`, or put them
|
|
65
|
+
in a shell profile or CI config. They are already stored; that is what this
|
|
66
|
+
command reads.
|
|
67
|
+
- **Never print a value.** If output somehow contains one, do not repeat it.
|
|
68
|
+
|
|
69
|
+
## Exit codes
|
|
70
|
+
|
|
71
|
+
- The command's own exit code, when it ran.
|
|
72
|
+
- `77` - the owner declined to authorize it. That is an answer, not a fault:
|
|
73
|
+
adapt or stop and explain where things stand. Do not retry the same command.
|
|
74
|
+
- `3` - nothing ran. No store is reachable here, or the runner went away.
|
|
75
|
+
|
|
76
|
+
## Writing a deploy skill
|
|
77
|
+
|
|
78
|
+
A repo's deploy skill declares every key it needs in
|
|
79
|
+
`.agents/skills/deploy/required-variables.json`, then wraps each step that
|
|
80
|
+
needs one in `factiii-secrets run -- …`. Declare a key exactly when a command
|
|
81
|
+
uses it: never declare one the skill does not use, never use one it does not
|
|
82
|
+
declare. Put no secret value, and no key material, in any file.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: share-a-port
|
|
3
|
+
description: Give a port running in this workspace a public HTTPS URL, with factiii-expose. TRIGGER when the user asks to share, expose, preview or publish a local server, wants a link to something running on localhost, or needs an external service to reach a local port.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# share-a-port
|
|
7
|
+
|
|
8
|
+
This workspace can put a local port on the public internet. Both commands are
|
|
9
|
+
already on your PATH; nothing needs installing.
|
|
10
|
+
|
|
11
|
+
## Share a port
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
factiii-expose <port> <name> # e.g. factiii-expose 3000 app
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
It prints the public URL and returns straight away - the tunnel runs in the
|
|
18
|
+
background and lives until the session ends or you stop it.
|
|
19
|
+
|
|
20
|
+
- `<name>` may hold letters, numbers and dashes only.
|
|
21
|
+
- Re-running it with the same name replaces that tunnel rather than opening a
|
|
22
|
+
second one, so it is safe to repeat.
|
|
23
|
+
- Start the server FIRST. A URL for a port nothing is listening on answers
|
|
24
|
+
with an error, which reads to the user like a broken tunnel.
|
|
25
|
+
|
|
26
|
+
## Stop sharing
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
factiii-unexpose <name>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Before you share
|
|
33
|
+
|
|
34
|
+
The URL is public and unauthenticated: anyone holding the link reaches
|
|
35
|
+
whatever that port serves. Do not expose a service that fronts real
|
|
36
|
+
credentials, customer data, or a database. If you are unsure whether the user
|
|
37
|
+
wants something reachable from outside, ask first.
|
|
38
|
+
|
|
39
|
+
Always tell the user the URL you got back. They can also see every live
|
|
40
|
+
tunnel, and stop any of them, in the card's Task Manager panel.
|
|
41
|
+
|