osuite 2.9.4 → 2.9.5
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/README.md +8 -1
- package/cli.js +167 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OSuite SDK (v2.9.
|
|
1
|
+
# OSuite SDK (v2.9.5)
|
|
2
2
|
|
|
3
3
|
**Governed action SDK for AI agents.**
|
|
4
4
|
|
|
@@ -11,6 +11,13 @@ The OSuite SDK helps teams route approvals, record governed decisions, and produ
|
|
|
11
11
|
npm install osuite
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
### One-command runtime connect
|
|
15
|
+
```bash
|
|
16
|
+
curl -fsSL https://studio.osuite.ai/install.sh | bash
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The installer detects Codex, Claude Code, SDK, or MCP-style projects, opens a browser handoff, and writes the approved runtime environment into the project (`.codex/.env`, `.claude/.env`, or `.osuite/.env`). API keys remain available as a fallback, but the normal path is terminal start -> Studio approval -> terminal auto-finish.
|
|
20
|
+
|
|
14
21
|
### Python
|
|
15
22
|
```bash
|
|
16
23
|
pip install requests
|
package/cli.js
CHANGED
|
@@ -162,6 +162,124 @@ function buildConnectHandoffUrl({ baseUrl, runtime, code }) {
|
|
|
162
162
|
return url.toString();
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function defaultAgentIdForRuntime(runtime) {
|
|
166
|
+
if (runtime === 'codex') return 'codex-runtime';
|
|
167
|
+
if (runtime === 'claude') return 'claude-code-runtime';
|
|
168
|
+
if (runtime === 'mcp') return 'mcp-runtime';
|
|
169
|
+
return 'sdk-runtime';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function envPathForRuntime(runtime) {
|
|
173
|
+
if (runtime === 'codex') return '.codex/.env';
|
|
174
|
+
if (runtime === 'claude') return '.claude/.env';
|
|
175
|
+
return '.osuite/.env';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function orderedEnvEntries(env = {}) {
|
|
179
|
+
const preferred = [
|
|
180
|
+
'OSUITE_BASE_URL',
|
|
181
|
+
'OSUITE_API_KEY',
|
|
182
|
+
'OSUITE_AGENT_ID',
|
|
183
|
+
'OSUITE_RUNTIME_ADAPTER_ID',
|
|
184
|
+
'OSUITE_SIGNATURE_MODE',
|
|
185
|
+
];
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const entries = [];
|
|
188
|
+
preferred.forEach((key) => {
|
|
189
|
+
if (env[key] !== undefined && env[key] !== null && env[key] !== '') {
|
|
190
|
+
seen.add(key);
|
|
191
|
+
entries.push([key, env[key]]);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
Object.keys(env)
|
|
195
|
+
.sort()
|
|
196
|
+
.forEach((key) => {
|
|
197
|
+
if (!seen.has(key) && env[key] !== undefined && env[key] !== null && env[key] !== '') {
|
|
198
|
+
entries.push([key, env[key]]);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return entries;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatEnvBody(env = {}) {
|
|
205
|
+
return `${orderedEnvEntries(env)
|
|
206
|
+
.map(([key, value]) => `${key}=${String(value).replace(/\n/g, '')}`)
|
|
207
|
+
.join('\n')}\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parsePositiveInt(value, fallback) {
|
|
211
|
+
const parsed = Number.parseInt(value, 10);
|
|
212
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
213
|
+
return parsed;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sleep(ms) {
|
|
217
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function postJson(url, body) {
|
|
221
|
+
const response = await fetch(url, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'content-type': 'application/json' },
|
|
224
|
+
body: JSON.stringify(body || {}),
|
|
225
|
+
});
|
|
226
|
+
const text = await response.text();
|
|
227
|
+
let payload = {};
|
|
228
|
+
if (text) {
|
|
229
|
+
try {
|
|
230
|
+
payload = JSON.parse(text);
|
|
231
|
+
} catch {
|
|
232
|
+
payload = { raw: text };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { response, payload };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function startDeviceConnect({ baseUrl, runtime, agentId }) {
|
|
239
|
+
const { response, payload } = await postJson(`${normalizedBaseUrl(baseUrl)}/api/onboarding/device-connect/start`, {
|
|
240
|
+
runtime,
|
|
241
|
+
agent_id: agentId,
|
|
242
|
+
project_root: process.cwd(),
|
|
243
|
+
});
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw new Error(payload.error || `Unable to start browser handoff: HTTP ${response.status}`);
|
|
246
|
+
}
|
|
247
|
+
if (!payload.device_code || !payload.poll_token || !payload.user_code) {
|
|
248
|
+
throw new Error('Studio did not return a complete device-connect payload.');
|
|
249
|
+
}
|
|
250
|
+
return payload;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function pollDeviceConnect({ baseUrl, deviceCode, pollToken, timeoutMs, intervalMs }) {
|
|
254
|
+
const deadline = Date.now() + timeoutMs;
|
|
255
|
+
while (Date.now() <= deadline) {
|
|
256
|
+
const { response, payload } = await postJson(`${normalizedBaseUrl(baseUrl)}/api/onboarding/device-connect/poll`, {
|
|
257
|
+
device_code: deviceCode,
|
|
258
|
+
poll_token: pollToken,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (response.status === 200 && payload.status === 'approved' && payload.credential?.env) {
|
|
262
|
+
return payload.credential;
|
|
263
|
+
}
|
|
264
|
+
if (response.status === 202 || payload.status === 'pending') {
|
|
265
|
+
const serverInterval = Number(payload.interval_seconds || 0) * 1000;
|
|
266
|
+
await sleep(serverInterval > 0 ? Math.max(intervalMs, serverInterval) : intervalMs);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (response.status === 410 || payload.status === 'expired') {
|
|
270
|
+
throw new Error('Browser handoff expired. Run osuite connect again.');
|
|
271
|
+
}
|
|
272
|
+
if (response.status === 409 || payload.status === 'consumed') {
|
|
273
|
+
throw new Error(payload.error || 'Browser handoff was already consumed.');
|
|
274
|
+
}
|
|
275
|
+
if (response.status === 404) {
|
|
276
|
+
throw new Error('Browser handoff was not found. Run osuite connect again.');
|
|
277
|
+
}
|
|
278
|
+
throw new Error(payload.error || `Unable to finish browser handoff: HTTP ${response.status}`);
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
165
283
|
function projectFilePath(relativePath) {
|
|
166
284
|
const clean = String(relativePath || '').replace(/^\/+/, '');
|
|
167
285
|
const root = path.resolve(process.cwd());
|
|
@@ -554,21 +672,32 @@ async function connect(target = 'auto', args = {}) {
|
|
|
554
672
|
const config = readConfig();
|
|
555
673
|
const runtime = detectRuntimeTarget(target);
|
|
556
674
|
const baseUrl = normalizedBaseUrl(args['base-url'] || config.baseUrl || DEFAULT_BASE_URL);
|
|
557
|
-
const
|
|
558
|
-
const
|
|
675
|
+
const configuredAgentId = envValue('OSUITE_AGENT_ID', 'DASHCLAW_AGENT_ID');
|
|
676
|
+
const agentId = String(args['agent-id'] || configuredAgentId || defaultAgentIdForRuntime(runtime));
|
|
677
|
+
const dryRunCode = String(args.code || generateConnectCode()).trim() || generateConnectCode();
|
|
678
|
+
let connectPayload = null;
|
|
679
|
+
let code = dryRunCode;
|
|
680
|
+
let handoffUrl = buildConnectHandoffUrl({ baseUrl, runtime, code });
|
|
559
681
|
const hasApiKey = Boolean(config.apiKey);
|
|
560
682
|
|
|
683
|
+
if (!args['dry-run']) {
|
|
684
|
+
connectPayload = await startDeviceConnect({ baseUrl, runtime, agentId });
|
|
685
|
+
code = connectPayload.user_code;
|
|
686
|
+
handoffUrl = connectPayload.verification_uri_complete || buildConnectHandoffUrl({ baseUrl, runtime, code });
|
|
687
|
+
}
|
|
688
|
+
|
|
561
689
|
write(c('OSuite Connect', `${ANSI.bold}${ANSI.cyan}`));
|
|
562
690
|
write(c('Project-root setup for governed agent actions.', ANSI.dim));
|
|
563
691
|
write('');
|
|
564
692
|
write(`Detected runtime ${runtime}`);
|
|
565
693
|
write(`Project root ${process.cwd()}`);
|
|
566
694
|
write(`Browser handoff ${handoffUrl}`);
|
|
695
|
+
write(`One-time code ${code}`);
|
|
567
696
|
write('');
|
|
568
697
|
write(c('What happens next', ANSI.bold));
|
|
569
|
-
write(' 1. Open the browser handoff URL and
|
|
570
|
-
write(' 2. OSuite prepares the runtime identity
|
|
571
|
-
write(' 3.
|
|
698
|
+
write(' 1. Open the browser handoff URL and approve this terminal connection in Studio.');
|
|
699
|
+
write(' 2. OSuite prepares the runtime identity, project hook lane, and signed env for this project.');
|
|
700
|
+
write(' 3. Return here; the CLI will finish automatically and then you can run osuite doctor.');
|
|
572
701
|
write('');
|
|
573
702
|
|
|
574
703
|
if (PROJECT_BOOTSTRAPS[runtime]) {
|
|
@@ -584,6 +713,39 @@ async function connect(target = 'auto', args = {}) {
|
|
|
584
713
|
if (runtime === 'mcp') write(' Wrap consequential tool calls with OSuite preflight, wait-for-approval, and complete-action steps.');
|
|
585
714
|
}
|
|
586
715
|
|
|
716
|
+
if (!args['dry-run'] && connectPayload) {
|
|
717
|
+
const timeoutMs = parsePositiveInt(args['poll-timeout-ms'], 180000);
|
|
718
|
+
const intervalMs = parsePositiveInt(
|
|
719
|
+
args['poll-interval-ms'],
|
|
720
|
+
Math.max(1000, Number(connectPayload.interval_seconds || 2) * 1000),
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
write('');
|
|
724
|
+
write(c('Waiting for browser approval', ANSI.bold));
|
|
725
|
+
write(` Code ${code} expires in about ${Math.round(Number(connectPayload.expires_in || 600) / 60)} minutes.`);
|
|
726
|
+
|
|
727
|
+
const credential = await pollDeviceConnect({
|
|
728
|
+
baseUrl,
|
|
729
|
+
deviceCode: connectPayload.device_code,
|
|
730
|
+
pollToken: connectPayload.poll_token,
|
|
731
|
+
timeoutMs,
|
|
732
|
+
intervalMs,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
if (credential?.env) {
|
|
736
|
+
const envPath = envPathForRuntime(runtime);
|
|
737
|
+
const result = writeProjectFile(envPath, formatEnvBody(credential.env), { force: true });
|
|
738
|
+
write('');
|
|
739
|
+
write(c('Connected to OSuite', ANSI.green));
|
|
740
|
+
write(` ${result.status === 'written' ? 'wrote' : 'kept'} ${result.relativePath}`);
|
|
741
|
+
write(' Run osuite doctor, then start the agent from this project root.');
|
|
742
|
+
} else {
|
|
743
|
+
write('');
|
|
744
|
+
write(c('Browser approval not completed before the timeout.', ANSI.yellow));
|
|
745
|
+
write(' Keep the browser handoff open or run osuite connect again.');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
587
749
|
write('');
|
|
588
750
|
write(c('API key fallback', ANSI.bold));
|
|
589
751
|
if (hasApiKey) {
|