@cowork-trust/trust 0.1.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/bin/trust.js +344 -0
- package/package.json +17 -0
package/bin/trust.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const command = process.argv[2];
|
|
9
|
+
const args = parseArgs(process.argv.slice(3));
|
|
10
|
+
let configCache;
|
|
11
|
+
|
|
12
|
+
main().catch((error) => {
|
|
13
|
+
console.error(JSON.stringify({ error: error.message }));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
if (!command || command === 'help' || command === '--help') {
|
|
19
|
+
printHelp();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
switch (command) {
|
|
24
|
+
case 'smoke':
|
|
25
|
+
await smoke();
|
|
26
|
+
return;
|
|
27
|
+
case 'evaluate':
|
|
28
|
+
await evaluate();
|
|
29
|
+
return;
|
|
30
|
+
case 'score':
|
|
31
|
+
await score();
|
|
32
|
+
return;
|
|
33
|
+
case 'feedback':
|
|
34
|
+
await feedback();
|
|
35
|
+
return;
|
|
36
|
+
case 'login':
|
|
37
|
+
await login();
|
|
38
|
+
return;
|
|
39
|
+
case 'logout':
|
|
40
|
+
await logout();
|
|
41
|
+
return;
|
|
42
|
+
case 'whoami':
|
|
43
|
+
case 'status':
|
|
44
|
+
await whoami();
|
|
45
|
+
return;
|
|
46
|
+
case 'config':
|
|
47
|
+
await configCommand();
|
|
48
|
+
return;
|
|
49
|
+
case 'keys':
|
|
50
|
+
await keysCommand();
|
|
51
|
+
return;
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unknown command: ${command}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function login() {
|
|
58
|
+
const apiUrl = required('api-url').replace(/\/$/, '');
|
|
59
|
+
const started = await requestRaw(apiUrl, '/api/cli/auth/start', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ apiUrl }),
|
|
63
|
+
});
|
|
64
|
+
print({ status: 'pending', browserUrl: started.browserUrl, userCode: started.userCode, expiresAt: started.expiresAt });
|
|
65
|
+
if (args.open !== 'false' && args['no-open'] !== 'true' && process.env.COWORK_TRUST_SKIP_BROWSER !== 'true') {
|
|
66
|
+
openBrowser(started.browserUrl);
|
|
67
|
+
}
|
|
68
|
+
const intervalMs = Number(args['poll-interval-ms'] || process.env.COWORK_TRUST_POLL_INTERVAL_MS || 2000);
|
|
69
|
+
const timeoutMs = Number(args['poll-timeout-ms'] || process.env.COWORK_TRUST_POLL_TIMEOUT_MS || 10 * 60 * 1000);
|
|
70
|
+
const deadline = Date.now() + timeoutMs;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
await sleep(intervalMs);
|
|
73
|
+
const result = await requestRaw(apiUrl, `/api/cli/auth/${encodeURIComponent(started.sessionId)}/poll`, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: { authorization: `Bearer ${started.pollToken}` },
|
|
76
|
+
});
|
|
77
|
+
if (result.status === 'consumed' && result.apiKey) {
|
|
78
|
+
const config = {
|
|
79
|
+
apiUrl,
|
|
80
|
+
apiKey: result.apiKey,
|
|
81
|
+
workspaceId: result.workspaceId,
|
|
82
|
+
authMode: 'api_key',
|
|
83
|
+
lastLoginAt: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
writeConfig(config);
|
|
86
|
+
print({ status: 'logged_in', apiUrl, workspaceId: result.workspaceId, configPath: configPath() });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (result.status === 'expired') throw new Error('CLI login expired. Run login again.');
|
|
90
|
+
}
|
|
91
|
+
throw new Error('CLI login timed out. Run login again.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function logout() {
|
|
95
|
+
if (existsSync(configPath())) rmSync(configPath());
|
|
96
|
+
configCache = {};
|
|
97
|
+
print({ status: 'logged_out' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function whoami() {
|
|
101
|
+
print(await get('/api/auth/whoami'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function configCommand() {
|
|
105
|
+
const subcommand = process.argv[3];
|
|
106
|
+
if (subcommand === 'get') {
|
|
107
|
+
const config = readConfig();
|
|
108
|
+
print({ ...config, apiKey: config.apiKey ? mask(config.apiKey) : undefined, configPath: configPath() });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (subcommand === 'set') {
|
|
112
|
+
const key = process.argv[4];
|
|
113
|
+
const value = process.argv[5];
|
|
114
|
+
if (key !== 'api-url') throw new Error('Only `cowork-trust config set api-url <url>` is supported');
|
|
115
|
+
if (!value) throw new Error('api-url value is required');
|
|
116
|
+
writeConfig({ ...readConfig(), apiUrl: value.replace(/\/$/, '') });
|
|
117
|
+
print({ status: 'updated', apiUrl: value.replace(/\/$/, ''), configPath: configPath() });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Usage: cowork-trust config get | cowork-trust config set api-url <url>');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function keysCommand() {
|
|
124
|
+
const subcommand = process.argv[3];
|
|
125
|
+
if (subcommand === 'list') {
|
|
126
|
+
print(await get('/api/workspace/api-keys'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (subcommand === 'create') {
|
|
130
|
+
const name = required('name');
|
|
131
|
+
print(await post('/api/workspace/api-keys', {
|
|
132
|
+
name,
|
|
133
|
+
environment: args.environment || 'test',
|
|
134
|
+
}));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (subcommand === 'revoke' || subcommand === 'rotate') {
|
|
138
|
+
const id = process.argv[4] || args.id;
|
|
139
|
+
if (!id) throw new Error(`Usage: cowork-trust keys ${subcommand} <key-id>`);
|
|
140
|
+
print(await post(`/api/workspace/api-keys/${encodeURIComponent(id)}/${subcommand}`, {}));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
throw new Error('Usage: cowork-trust keys list | keys create --name <name> [--environment test|live] | keys revoke <key-id> | keys rotate <key-id>');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function smoke() {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
const checks = [
|
|
149
|
+
{
|
|
150
|
+
name: 'read',
|
|
151
|
+
expected: 'allow',
|
|
152
|
+
payload: payload('resource.read', 'read', 'record:smoke-read', `smoke-read-${now}`),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'write',
|
|
156
|
+
expected: 'require_approval',
|
|
157
|
+
payload: payload('resource.write', 'write', 'record:smoke-write', `smoke-write-${now}`),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'delete',
|
|
161
|
+
expected: 'deny',
|
|
162
|
+
payload: payload('resource.delete', 'delete', 'record:smoke-delete', `smoke-delete-${now}`),
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const results = [];
|
|
167
|
+
for (const check of checks) {
|
|
168
|
+
const result = await post('/api/trust/evaluate', check.payload);
|
|
169
|
+
results.push({ name: check.name, expected: check.expected, decision: result.decision, evaluationId: result.evaluationId });
|
|
170
|
+
if (result.decision !== check.expected) {
|
|
171
|
+
throw new Error(`Smoke check ${check.name} expected ${check.expected} but received ${result.decision}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
print(results);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function evaluate() {
|
|
178
|
+
const category = required('category');
|
|
179
|
+
const action = required('action');
|
|
180
|
+
const resource = required('resource');
|
|
181
|
+
print(await post('/api/trust/evaluate', payload(action, category, resource, args['idempotency-key'])));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function score() {
|
|
185
|
+
const actor = encodeURIComponent(required('actor'));
|
|
186
|
+
print(await get(`/api/trust/actors/${actor}/score`));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function feedback() {
|
|
190
|
+
const evaluation = encodeURIComponent(required('evaluation'));
|
|
191
|
+
const signalType = required('signal');
|
|
192
|
+
print(await post(`/api/trust/evaluations/${evaluation}/feedback`, {
|
|
193
|
+
signalType,
|
|
194
|
+
reason: args.reason,
|
|
195
|
+
idempotencyKey: args['idempotency-key'],
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function payload(actionType, category, resourceValue, idempotencyKey) {
|
|
200
|
+
const [resourceType, resourceId] = String(resourceValue).split(':');
|
|
201
|
+
return {
|
|
202
|
+
workspaceId: option('workspace') || option('workspace-id') || 'cli-workspace',
|
|
203
|
+
actor: {
|
|
204
|
+
externalId: option('actor') || 'cli-agent',
|
|
205
|
+
type: 'agent',
|
|
206
|
+
platform: 'cli',
|
|
207
|
+
},
|
|
208
|
+
action: {
|
|
209
|
+
type: actionType,
|
|
210
|
+
category,
|
|
211
|
+
},
|
|
212
|
+
resource: {
|
|
213
|
+
type: resourceType,
|
|
214
|
+
id: resourceId,
|
|
215
|
+
},
|
|
216
|
+
idempotencyKey,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function get(path) {
|
|
221
|
+
return request(path, { method: 'GET' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function post(path, body) {
|
|
225
|
+
return request(path, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'content-type': 'application/json' },
|
|
228
|
+
body: JSON.stringify(body),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function request(path, init) {
|
|
233
|
+
const apiUrl = required('api-url').replace(/\/$/, '');
|
|
234
|
+
const apiKey = required('api-key');
|
|
235
|
+
return requestRaw(apiUrl, path, {
|
|
236
|
+
...init,
|
|
237
|
+
headers: {
|
|
238
|
+
...(init.headers || {}),
|
|
239
|
+
authorization: `Bearer ${apiKey}`,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function requestRaw(apiUrl, path, init) {
|
|
245
|
+
const response = await fetch(`${apiUrl}${path}`, init);
|
|
246
|
+
const text = await response.text();
|
|
247
|
+
const body = text ? JSON.parse(text) : {};
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
throw new Error(body.message || `Request failed with status ${response.status}`);
|
|
250
|
+
}
|
|
251
|
+
return body;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseArgs(values) {
|
|
255
|
+
const parsed = {};
|
|
256
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
257
|
+
const value = values[index];
|
|
258
|
+
if (!value.startsWith('--')) continue;
|
|
259
|
+
const key = value.slice(2);
|
|
260
|
+
const next = values[index + 1];
|
|
261
|
+
if (!next || next.startsWith('--')) {
|
|
262
|
+
parsed[key] = 'true';
|
|
263
|
+
} else {
|
|
264
|
+
parsed[key] = next;
|
|
265
|
+
index += 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return parsed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function required(name) {
|
|
272
|
+
const value = option(name);
|
|
273
|
+
if (!value) {
|
|
274
|
+
if (name === 'api-key') throw new Error('Missing API key. Run `cowork-trust login --api-url <url>` or pass --api-key.');
|
|
275
|
+
if (name === 'api-url') throw new Error('Missing API URL. Run `cowork-trust login --api-url <url>` or pass --api-url.');
|
|
276
|
+
throw new Error(`Missing required option --${name}`);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function option(name) {
|
|
282
|
+
const envName = `COWORK_${name.replaceAll('-', '_').toUpperCase()}`;
|
|
283
|
+
const configKey = name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
284
|
+
return args[name] || process.env[envName] || readConfig()[configKey];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readConfig() {
|
|
288
|
+
if (configCache) return configCache;
|
|
289
|
+
const path = configPath();
|
|
290
|
+
if (!existsSync(path)) {
|
|
291
|
+
configCache = {};
|
|
292
|
+
return configCache;
|
|
293
|
+
}
|
|
294
|
+
configCache = JSON.parse(readFileSync(path, 'utf8'));
|
|
295
|
+
return configCache;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function writeConfig(config) {
|
|
299
|
+
const path = configPath();
|
|
300
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
301
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
302
|
+
configCache = config;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function configPath() {
|
|
306
|
+
return process.env.COWORK_TRUST_CONFIG || join(homedir(), '.cowork', 'trust', 'config.json');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function openBrowser(url) {
|
|
310
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
311
|
+
const commandArgs = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
312
|
+
const child = spawn(command, commandArgs, { detached: true, stdio: 'ignore' });
|
|
313
|
+
child.unref();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sleep(ms) {
|
|
317
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function mask(value) {
|
|
321
|
+
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function print(value) {
|
|
325
|
+
console.log(JSON.stringify(value));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function printHelp() {
|
|
329
|
+
console.log(`Usage:
|
|
330
|
+
cowork-trust login --api-url <url>
|
|
331
|
+
cowork-trust logout
|
|
332
|
+
cowork-trust whoami
|
|
333
|
+
cowork-trust status
|
|
334
|
+
cowork-trust config get
|
|
335
|
+
cowork-trust config set api-url <url>
|
|
336
|
+
cowork-trust keys list
|
|
337
|
+
cowork-trust keys create --name connector-prod [--environment test|live]
|
|
338
|
+
cowork-trust keys revoke <key-id>
|
|
339
|
+
cowork-trust keys rotate <key-id>
|
|
340
|
+
cowork-trust smoke [--api-url <url>] [--api-key <key>]
|
|
341
|
+
cowork-trust evaluate --category read --action resource.read --resource record:123
|
|
342
|
+
cowork-trust score --actor agent-1
|
|
343
|
+
cowork-trust feedback --evaluation <id> --signal approved`);
|
|
344
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cowork-trust/trust",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cowork-trust": "bin/trust.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"build": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin"
|
|
16
|
+
]
|
|
17
|
+
}
|