@crewx/shared 0.0.6-rc.21 → 0.0.6-rc.23

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.
@@ -0,0 +1,14 @@
1
+ export type RuntimeCredentialPurpose = 'notify' | 'wi' | 'chromex-negotiate' | 'pairing';
2
+ export interface ResolvedCredential {
3
+ token: string;
4
+ source: 'env' | 'file';
5
+ }
6
+ type CrewxCredentialErrorCode = 'MISSING' | 'BAD_MODE' | 'UNREADABLE';
7
+ export declare class CrewxCredentialError extends Error {
8
+ readonly code: CrewxCredentialErrorCode;
9
+ readonly exitCode: number;
10
+ constructor(code: CrewxCredentialErrorCode, message: string);
11
+ }
12
+ export declare function resolveCrewxApiCredential(purpose: RuntimeCredentialPurpose, env?: NodeJS.ProcessEnv): ResolvedCredential;
13
+ export declare function authorizationHeader(cred: ResolvedCredential): Record<'Authorization', string>;
14
+ export {};
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CrewxCredentialError = void 0;
37
+ exports.resolveCrewxApiCredential = resolveCrewxApiCredential;
38
+ exports.authorizationHeader = authorizationHeader;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const sdk_1 = require("@crewx/sdk");
42
+ const RUNTIME_CREDENTIAL_PURPOSES = new Set([
43
+ 'notify',
44
+ 'wi',
45
+ 'chromex-negotiate',
46
+ 'pairing',
47
+ ]);
48
+ // These are sysexits-style values so shell callers can distinguish the local
49
+ // credential failure before an HTTP request is attempted.
50
+ const EXIT_CODES = {
51
+ MISSING: 78,
52
+ BAD_MODE: 77,
53
+ UNREADABLE: 74,
54
+ };
55
+ const HEX_64 = '[0-9a-f]{64}';
56
+ class CrewxCredentialError extends Error {
57
+ code;
58
+ exitCode;
59
+ constructor(code, message) {
60
+ super(message);
61
+ this.name = 'CrewxCredentialError';
62
+ this.code = code;
63
+ this.exitCode = EXIT_CODES[code];
64
+ Object.setPrototypeOf(this, new.target.prototype);
65
+ }
66
+ }
67
+ exports.CrewxCredentialError = CrewxCredentialError;
68
+ function isErrnoCode(error, code) {
69
+ return typeof error === 'object'
70
+ && error !== null
71
+ && 'code' in error
72
+ && error.code === code;
73
+ }
74
+ function credentialPath(purpose) {
75
+ return path.join((0, sdk_1.getCrewxHome)(), 'auth', 'runtime', `${purpose}.token`);
76
+ }
77
+ function invalidPurpose(purpose) {
78
+ throw new TypeError(`Unsupported runtime credential purpose: ${String(purpose)}`);
79
+ }
80
+ function missingCredential(purpose, filePath) {
81
+ throw new CrewxCredentialError('MISSING', `Runtime credential '${purpose}' is missing at ${filePath}. `
82
+ + 'Run the CrewX server once to create it or set CREWX_API_TOKEN.');
83
+ }
84
+ function badMode(purpose, filePath) {
85
+ throw new CrewxCredentialError('BAD_MODE', `Runtime credential '${purpose}' at ${filePath} has insecure permissions; expected mode 0600.`);
86
+ }
87
+ function unreadableCredential(purpose, filePath) {
88
+ throw new CrewxCredentialError('UNREADABLE', `Runtime credential '${purpose}' at ${filePath} is unreadable or invalid. `
89
+ + 'Check the file contents and permissions.');
90
+ }
91
+ function readRuntimeCredential(purpose) {
92
+ const filePath = credentialPath(purpose);
93
+ let stats;
94
+ try {
95
+ // lstat prevents a symlink from bypassing the mode check on POSIX.
96
+ stats = fs.lstatSync(filePath);
97
+ }
98
+ catch (error) {
99
+ if (isErrnoCode(error, 'ENOENT'))
100
+ missingCredential(purpose, filePath);
101
+ unreadableCredential(purpose, filePath);
102
+ }
103
+ if (!stats.isFile())
104
+ unreadableCredential(purpose, filePath);
105
+ // Windows does not expose POSIX mode semantics; the server contract skips
106
+ // this check there as well.
107
+ if (process.platform !== 'win32' && (stats.mode & 0o777) !== 0o600) {
108
+ badMode(purpose, filePath);
109
+ }
110
+ let token;
111
+ try {
112
+ token = fs.readFileSync(filePath, 'utf8');
113
+ }
114
+ catch (error) {
115
+ if (isErrnoCode(error, 'ENOENT'))
116
+ missingCredential(purpose, filePath);
117
+ unreadableCredential(purpose, filePath);
118
+ }
119
+ const expected = new RegExp(`^crewxrt_${purpose}_${HEX_64}$`);
120
+ if (!expected.test(token))
121
+ unreadableCredential(purpose, filePath);
122
+ return token;
123
+ }
124
+ function resolveCrewxApiCredential(purpose, env = process.env) {
125
+ if (!RUNTIME_CREDENTIAL_PURPOSES.has(purpose))
126
+ invalidPurpose(purpose);
127
+ const envToken = env['CREWX_API_TOKEN'];
128
+ if (typeof envToken === 'string' && envToken.length > 0) {
129
+ return { token: envToken, source: 'env' };
130
+ }
131
+ return { token: readRuntimeCredential(purpose), source: 'file' };
132
+ }
133
+ function authorizationHeader(cred) {
134
+ return { Authorization: `Bearer ${cred.token}` };
135
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { getCrewxHome } from '@crewx/sdk';
4
+
5
+ export type RuntimeCredentialPurpose = 'notify' | 'wi' | 'chromex-negotiate' | 'pairing';
6
+
7
+ export interface ResolvedCredential {
8
+ token: string;
9
+ source: 'env' | 'file';
10
+ }
11
+
12
+ type CrewxCredentialErrorCode = 'MISSING' | 'BAD_MODE' | 'UNREADABLE';
13
+
14
+ const RUNTIME_CREDENTIAL_PURPOSES = new Set<RuntimeCredentialPurpose>([
15
+ 'notify',
16
+ 'wi',
17
+ 'chromex-negotiate',
18
+ 'pairing',
19
+ ]);
20
+
21
+ // These are sysexits-style values so shell callers can distinguish the local
22
+ // credential failure before an HTTP request is attempted.
23
+ const EXIT_CODES: Record<CrewxCredentialErrorCode, number> = {
24
+ MISSING: 78,
25
+ BAD_MODE: 77,
26
+ UNREADABLE: 74,
27
+ };
28
+
29
+ const HEX_64 = '[0-9a-f]{64}';
30
+
31
+ export class CrewxCredentialError extends Error {
32
+ readonly code: CrewxCredentialErrorCode;
33
+ readonly exitCode: number;
34
+
35
+ constructor(code: CrewxCredentialErrorCode, message: string) {
36
+ super(message);
37
+ this.name = 'CrewxCredentialError';
38
+ this.code = code;
39
+ this.exitCode = EXIT_CODES[code];
40
+ Object.setPrototypeOf(this, new.target.prototype);
41
+ }
42
+ }
43
+
44
+ function isErrnoCode(error: unknown, code: string): boolean {
45
+ return typeof error === 'object'
46
+ && error !== null
47
+ && 'code' in error
48
+ && (error as { code?: unknown }).code === code;
49
+ }
50
+
51
+ function credentialPath(purpose: RuntimeCredentialPurpose): string {
52
+ return path.join(getCrewxHome(), 'auth', 'runtime', `${purpose}.token`);
53
+ }
54
+
55
+ function invalidPurpose(purpose: RuntimeCredentialPurpose): never {
56
+ throw new TypeError(`Unsupported runtime credential purpose: ${String(purpose)}`);
57
+ }
58
+
59
+ function missingCredential(purpose: RuntimeCredentialPurpose, filePath: string): never {
60
+ throw new CrewxCredentialError(
61
+ 'MISSING',
62
+ `Runtime credential '${purpose}' is missing at ${filePath}. `
63
+ + 'Run the CrewX server once to create it or set CREWX_API_TOKEN.',
64
+ );
65
+ }
66
+
67
+ function badMode(purpose: RuntimeCredentialPurpose, filePath: string): never {
68
+ throw new CrewxCredentialError(
69
+ 'BAD_MODE',
70
+ `Runtime credential '${purpose}' at ${filePath} has insecure permissions; expected mode 0600.`,
71
+ );
72
+ }
73
+
74
+ function unreadableCredential(purpose: RuntimeCredentialPurpose, filePath: string): never {
75
+ throw new CrewxCredentialError(
76
+ 'UNREADABLE',
77
+ `Runtime credential '${purpose}' at ${filePath} is unreadable or invalid. `
78
+ + 'Check the file contents and permissions.',
79
+ );
80
+ }
81
+
82
+ function readRuntimeCredential(purpose: RuntimeCredentialPurpose): string {
83
+ const filePath = credentialPath(purpose);
84
+ let stats: fs.Stats;
85
+
86
+ try {
87
+ // lstat prevents a symlink from bypassing the mode check on POSIX.
88
+ stats = fs.lstatSync(filePath);
89
+ } catch (error: unknown) {
90
+ if (isErrnoCode(error, 'ENOENT')) missingCredential(purpose, filePath);
91
+ unreadableCredential(purpose, filePath);
92
+ }
93
+
94
+ if (!stats!.isFile()) unreadableCredential(purpose, filePath);
95
+
96
+ // Windows does not expose POSIX mode semantics; the server contract skips
97
+ // this check there as well.
98
+ if (process.platform !== 'win32' && (stats!.mode & 0o777) !== 0o600) {
99
+ badMode(purpose, filePath);
100
+ }
101
+
102
+ let token: string;
103
+ try {
104
+ token = fs.readFileSync(filePath, 'utf8');
105
+ } catch (error: unknown) {
106
+ if (isErrnoCode(error, 'ENOENT')) missingCredential(purpose, filePath);
107
+ unreadableCredential(purpose, filePath);
108
+ }
109
+
110
+ const expected = new RegExp(`^crewxrt_${purpose}_${HEX_64}$`);
111
+ if (!expected.test(token!)) unreadableCredential(purpose, filePath);
112
+ return token!;
113
+ }
114
+
115
+ export function resolveCrewxApiCredential(
116
+ purpose: RuntimeCredentialPurpose,
117
+ env: NodeJS.ProcessEnv = process.env,
118
+ ): ResolvedCredential {
119
+ if (!RUNTIME_CREDENTIAL_PURPOSES.has(purpose)) invalidPurpose(purpose);
120
+
121
+ const envToken = env['CREWX_API_TOKEN'];
122
+ if (typeof envToken === 'string' && envToken.length > 0) {
123
+ return { token: envToken, source: 'env' };
124
+ }
125
+
126
+ return { token: readRuntimeCredential(purpose), source: 'file' };
127
+ }
128
+
129
+ export function authorizationHeader(cred: ResolvedCredential): Record<'Authorization', string> {
130
+ return { Authorization: `Bearer ${cred.token}` };
131
+ }
package/package.json CHANGED
@@ -1,12 +1,41 @@
1
1
  {
2
2
  "name": "@crewx/shared",
3
- "version": "0.0.6-rc.21",
3
+ "version": "0.0.6-rc.23",
4
4
  "main": "skill-tracer.js",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./skill-tracer.d.ts",
8
+ "require": "./skill-tracer.js",
9
+ "default": "./skill-tracer.js"
10
+ },
11
+ "./skill-tracer": {
12
+ "types": "./skill-tracer.d.ts",
13
+ "require": "./skill-tracer.js",
14
+ "default": "./skill-tracer.js"
15
+ },
16
+ "./crewx-api-credential": {
17
+ "types": "./crewx-api-credential.d.ts",
18
+ "require": "./crewx-api-credential.js",
19
+ "default": "./crewx-api-credential.js"
20
+ }
21
+ },
22
+ "typesVersions": {
23
+ "*": {
24
+ "crewx-api-credential": [
25
+ "crewx-api-credential.d.ts"
26
+ ]
27
+ }
28
+ },
5
29
  "description": "Shared utilities for CrewX built-in packages",
6
30
  "dependencies": {
7
- "@crewx/sdk": "0.8.9-rc.34"
31
+ "@crewx/sdk": "0.9.0-rc.108"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "typescript": "^5.0.0"
8
36
  },
9
37
  "scripts": {
38
+ "build": "tsc -p tsconfig.json",
10
39
  "test": "echo no tests"
11
40
  }
12
41
  }
package/skill-tracer.js CHANGED
@@ -24,9 +24,7 @@ const { generateId } = require('@crewx/sdk');
24
24
  * crewx.db 경로 찾기
25
25
  */
26
26
  function getDbPath() {
27
- if (process.env.CREWX_TRACES_DB || process.env.CREWX_DB) {
28
- return process.env.CREWX_TRACES_DB || process.env.CREWX_DB;
29
- }
27
+ if (process.env.CREWX_DB) return process.env.CREWX_DB;
30
28
 
31
29
  return path.join(os.homedir(), '.crewx', 'crewx.db');
32
30
  }
@@ -35,141 +33,67 @@ function getDbPath() {
35
33
  const getTracesDbPath = getDbPath;
36
34
 
37
35
  /**
38
- * crewx.db 연결 테이블 확인
39
- * Returns null if better-sqlite3 is unavailable or DB cannot be opened.
36
+ * Open an already initialized crewx.db for trace writes.
37
+ * This function never creates a database, directory, or schema.
38
+ * Returns null if better-sqlite3 is unavailable, the database is missing, or
39
+ * the existing file does not contain the tracing tables used by this module.
40
40
  */
41
41
  function getDb() {
42
42
  if (!Database) return null;
43
43
 
44
44
  const dbPath = getDbPath();
45
- const dir = path.dirname(dbPath);
46
-
47
- if (!fs.existsSync(dir)) {
48
- fs.mkdirSync(dir, { recursive: true });
49
- }
45
+ if (!fs.existsSync(dbPath)) return null;
50
46
 
51
- const db = new Database(dbPath);
52
- db.exec('PRAGMA journal_mode = WAL');
53
- db.exec('PRAGMA busy_timeout = 5000');
54
- db.exec('PRAGMA foreign_keys = ON');
55
-
56
- db.exec(`
57
- CREATE TABLE IF NOT EXISTS tasks (
58
- id TEXT PRIMARY KEY,
59
- agent_id TEXT NOT NULL,
60
- user_id TEXT,
61
- prompt TEXT NOT NULL,
62
- mode TEXT NOT NULL DEFAULT 'execute',
63
- status TEXT NOT NULL DEFAULT 'running',
64
- result TEXT,
65
- error TEXT,
66
- started_at TEXT NOT NULL,
67
- completed_at TEXT,
68
- duration_ms INTEGER,
69
- metadata TEXT,
70
- project_id TEXT,
71
- project_name TEXT
72
- )
73
- `);
74
-
75
- db.exec(`
76
- CREATE TABLE IF NOT EXISTS spans (
77
- id TEXT PRIMARY KEY,
78
- task_id TEXT,
79
- parent_span_id TEXT,
80
- name TEXT NOT NULL,
81
- kind TEXT NOT NULL DEFAULT 'internal',
82
- status TEXT NOT NULL DEFAULT 'ok',
83
- started_at TEXT NOT NULL,
84
- completed_at TEXT,
85
- duration_ms INTEGER,
86
- input TEXT,
87
- output TEXT,
88
- error TEXT,
89
- attributes TEXT,
90
- FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
91
- FOREIGN KEY (parent_span_id) REFERENCES spans(id) ON DELETE SET NULL
92
- )
93
- `);
94
-
95
- ensureProjectColumns(db);
96
- ensureSpansTaskIdNullable(db);
97
-
98
- return db;
99
- }
100
-
101
- function ensureProjectColumns(db) {
47
+ let db;
102
48
  try {
103
- const columns = db.prepare('PRAGMA table_info(tasks)').all().map((col) => col.name);
104
- if (!columns.includes('project_id')) {
105
- db.exec(`ALTER TABLE tasks ADD COLUMN project_id TEXT`);
106
- }
107
- if (!columns.includes('project_name')) {
108
- db.exec(`ALTER TABLE tasks ADD COLUMN project_name TEXT`);
109
- }
110
- db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id)`);
111
- } catch {
112
- // Best-effort; ignore failures
113
- }
114
- }
49
+ db = new Database(dbPath, { fileMustExist: true });
50
+ db.exec('PRAGMA journal_mode = WAL');
51
+ db.exec('PRAGMA busy_timeout = 5000');
52
+ db.exec('PRAGMA foreign_keys = ON');
115
53
 
116
- function ensureSpansTaskIdNullable(db) {
117
- try {
118
- const columns = db.prepare('PRAGMA table_info(spans)').all();
119
- const taskIdColumn = columns.find((col) => col.name === 'task_id');
120
- if (!taskIdColumn || taskIdColumn.notnull === 0) {
121
- return;
54
+ if (!hasTraceSchema(db)) {
55
+ db.close();
56
+ return null;
122
57
  }
123
58
 
124
- db.exec('PRAGMA foreign_keys = OFF');
125
- db.exec('BEGIN');
126
- db.exec(`
127
- CREATE TABLE spans_backup (
128
- id TEXT PRIMARY KEY,
129
- task_id TEXT,
130
- parent_span_id TEXT,
131
- name TEXT NOT NULL,
132
- kind TEXT NOT NULL DEFAULT 'internal',
133
- status TEXT NOT NULL DEFAULT 'ok',
134
- started_at TEXT NOT NULL,
135
- completed_at TEXT,
136
- duration_ms INTEGER,
137
- input TEXT,
138
- output TEXT,
139
- error TEXT,
140
- attributes TEXT,
141
- FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
142
- FOREIGN KEY (parent_span_id) REFERENCES spans_backup(id) ON DELETE SET NULL
143
- )
144
- `);
145
- db.exec(`
146
- INSERT INTO spans_backup (
147
- id, task_id, parent_span_id, name, kind, status, started_at,
148
- completed_at, duration_ms, input, output, error, attributes
149
- )
150
- SELECT
151
- id, task_id, parent_span_id, name, kind, status, started_at,
152
- completed_at, duration_ms, input, output, error, attributes
153
- FROM spans
154
- `);
155
- db.exec('DROP TABLE spans');
156
- db.exec('ALTER TABLE spans_backup RENAME TO spans');
157
- db.exec('COMMIT');
158
- db.exec('PRAGMA foreign_keys = ON');
159
- } catch {
59
+ return db;
60
+ } catch (error) {
160
61
  try {
161
- db.exec('ROLLBACK');
62
+ db?.close();
162
63
  } catch {
163
- // Best-effort rollback
164
- }
165
- try {
166
- db.exec('PRAGMA foreign_keys = ON');
167
- } catch {
168
- // Best-effort; ignore failures
64
+ // Best-effort cleanup after an open or pragma failure.
169
65
  }
66
+ throw error;
170
67
  }
171
68
  }
172
69
 
70
+ function hasTraceSchema(db) {
71
+ const requiredColumns = {
72
+ tasks: ['id'],
73
+ spans: [
74
+ 'id',
75
+ 'task_id',
76
+ 'name',
77
+ 'kind',
78
+ 'status',
79
+ 'started_at',
80
+ 'completed_at',
81
+ 'duration_ms',
82
+ 'input',
83
+ 'output',
84
+ 'error',
85
+ 'attributes',
86
+ ],
87
+ };
88
+
89
+ return Object.entries(requiredColumns).every(([tableName, columns]) => {
90
+ const availableColumns = new Set(
91
+ db.prepare(`PRAGMA table_info(${tableName})`).all().map((column) => column.name),
92
+ );
93
+ return columns.every((column) => availableColumns.has(column));
94
+ });
95
+ }
96
+
173
97
  function resolveProjectContext() {
174
98
  const projectPath = path.resolve(process.cwd());
175
99
  return {
@@ -0,0 +1,110 @@
1
+ // @vitest-environment node
2
+
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import * as fs from 'node:fs';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ import {
8
+ authorizationHeader,
9
+ CrewxCredentialError,
10
+ resolveCrewxApiCredential,
11
+ } from '../crewx-api-credential';
12
+
13
+ const VALID_HEX = 'a'.repeat(64);
14
+
15
+ let tmpHome: string;
16
+ const savedEnv = { ...process.env };
17
+
18
+ function runtimePath(purpose: string): string {
19
+ return path.join(tmpHome, 'auth', 'runtime', `${purpose}.token`);
20
+ }
21
+
22
+ function writeCredential(purpose: string, mode = 0o600, token = `crewxrt_${purpose}_${VALID_HEX}`): void {
23
+ const file = runtimePath(purpose);
24
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
25
+ fs.writeFileSync(file, token, { encoding: 'utf8', mode });
26
+ fs.chmodSync(file, mode);
27
+ }
28
+
29
+ beforeEach(() => {
30
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'crewx-runtime-credential-'));
31
+ process.env['CREWX_HOME'] = tmpHome;
32
+ delete process.env['CREWX_API_TOKEN'];
33
+ });
34
+
35
+ afterEach(() => {
36
+ process.env = { ...savedEnv };
37
+ fs.rmSync(tmpHome, { recursive: true, force: true });
38
+ });
39
+
40
+ describe('resolveCrewxApiCredential', () => {
41
+ it('resolves a purpose-scoped 0600 runtime credential and builds its Bearer header', () => {
42
+ writeCredential('notify');
43
+
44
+ const credential = resolveCrewxApiCredential('notify');
45
+
46
+ expect(credential).toEqual({ token: `crewxrt_notify_${VALID_HEX}`, source: 'file' });
47
+ expect(authorizationHeader(credential)).toEqual({
48
+ Authorization: `Bearer crewxrt_notify_${VALID_HEX}`,
49
+ });
50
+ });
51
+
52
+ it('resolves the pairing runtime credential', () => {
53
+ writeCredential('pairing');
54
+
55
+ expect(resolveCrewxApiCredential('pairing')).toEqual({
56
+ token: `crewxrt_pairing_${VALID_HEX}`,
57
+ source: 'file',
58
+ });
59
+ });
60
+
61
+ it('prefers CREWX_API_TOKEN without inspecting the runtime credential file', () => {
62
+ writeCredential('wi', 0o644);
63
+ process.env['CREWX_API_TOKEN'] = 'env-token';
64
+
65
+ expect(resolveCrewxApiCredential('wi')).toEqual({ token: 'env-token', source: 'env' });
66
+ });
67
+
68
+ it('fails with MISSING and a non-zero exit code when the runtime file is absent', () => {
69
+ expect(() => resolveCrewxApiCredential('notify')).toThrow(CrewxCredentialError);
70
+
71
+ try {
72
+ resolveCrewxApiCredential('notify');
73
+ throw new Error('expected resolveCrewxApiCredential to throw');
74
+ } catch (error) {
75
+ expect(error).toMatchObject({ code: 'MISSING', exitCode: expect.any(Number) });
76
+ expect((error as Error).message).toContain('notify.token');
77
+ expect((error as CrewxCredentialError).exitCode).toBe(78);
78
+ }
79
+ });
80
+
81
+ it('fails with a distinct BAD_MODE error when the runtime file is not 0600', () => {
82
+ writeCredential('notify', 0o644);
83
+
84
+ expect(() => resolveCrewxApiCredential('notify')).toThrow(CrewxCredentialError);
85
+
86
+ try {
87
+ resolveCrewxApiCredential('notify');
88
+ throw new Error('expected resolveCrewxApiCredential to throw');
89
+ } catch (error) {
90
+ expect(error).toMatchObject({ code: 'BAD_MODE', exitCode: expect.any(Number) });
91
+ expect((error as Error).message).toContain('0600');
92
+ expect((error as CrewxCredentialError).exitCode).toBe(77);
93
+ }
94
+ });
95
+
96
+ it('fails with UNREADABLE when the file content does not match the purpose contract', () => {
97
+ writeCredential('chromex-negotiate', 0o600, 'not-a-runtime-token');
98
+
99
+ expect(() => resolveCrewxApiCredential('chromex-negotiate')).toThrow(CrewxCredentialError);
100
+
101
+ try {
102
+ resolveCrewxApiCredential('chromex-negotiate');
103
+ throw new Error('expected resolveCrewxApiCredential to throw');
104
+ } catch (error) {
105
+ expect(error).toMatchObject({ code: 'UNREADABLE', exitCode: expect.any(Number) });
106
+ expect((error as CrewxCredentialError).exitCode).toBe(74);
107
+ expect((error as Error).message).not.toContain('not-a-runtime-token');
108
+ }
109
+ });
110
+ });
@@ -6,6 +6,36 @@ import Database from 'better-sqlite3';
6
6
 
7
7
  const SPAN_ID_PATTERN = /^spn_[A-Za-z0-9]{8}$/;
8
8
 
9
+ function createTracerDb(dbPath: string): void {
10
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
11
+ const db = new Database(dbPath);
12
+ db.exec(`
13
+ CREATE TABLE tasks (
14
+ id TEXT PRIMARY KEY,
15
+ agent_id TEXT,
16
+ prompt TEXT,
17
+ status TEXT,
18
+ started_at TEXT
19
+ );
20
+ CREATE TABLE spans (
21
+ id TEXT PRIMARY KEY,
22
+ task_id TEXT,
23
+ parent_span_id TEXT,
24
+ name TEXT NOT NULL,
25
+ kind TEXT NOT NULL DEFAULT 'internal',
26
+ status TEXT NOT NULL DEFAULT 'ok',
27
+ started_at TEXT NOT NULL,
28
+ completed_at TEXT,
29
+ duration_ms INTEGER,
30
+ input TEXT,
31
+ output TEXT,
32
+ error TEXT,
33
+ attributes TEXT
34
+ );
35
+ `);
36
+ db.close();
37
+ }
38
+
9
39
  describe('shared/skill-tracer', () => {
10
40
  const sharedDir = path.resolve(__dirname, '..');
11
41
 
@@ -48,6 +78,7 @@ describe('shared/skill-tracer', () => {
48
78
 
49
79
  process.env.CREWX_DB = dbPath;
50
80
  delete process.env.CREWX_TASK_ID;
81
+ createTracerDb(dbPath);
51
82
 
52
83
  try {
53
84
  const trace = tracer.trace('memory', 'index core_sqa');
@@ -69,6 +100,39 @@ describe('shared/skill-tracer', () => {
69
100
  fs.rmSync(tempDir, { recursive: true, force: true });
70
101
  }
71
102
  });
103
+
104
+ it('does not create a missing target database and no-ops without throwing', () => {
105
+ const tracer = require(path.join(sharedDir, 'skill-tracer.js'));
106
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-tracer-missing-'));
107
+ const dbPath = path.join(tempDir, 'nested', 'crewx.db');
108
+ const previousDbPath = process.env.CREWX_DB;
109
+ const previousTaskId = process.env.CREWX_TASK_ID;
110
+
111
+ process.env.CREWX_DB = dbPath;
112
+ delete process.env.CREWX_TASK_ID;
113
+
114
+ try {
115
+ expect(() => {
116
+ const trace = tracer.trace('memory', 'index core_sqa');
117
+ expect(trace._skipped).toBe(true);
118
+ trace.ok('ignored');
119
+ }).not.toThrow();
120
+ expect(fs.existsSync(dbPath)).toBe(false);
121
+ expect(fs.existsSync(path.dirname(dbPath))).toBe(false);
122
+ } finally {
123
+ if (previousDbPath === undefined) {
124
+ delete process.env.CREWX_DB;
125
+ } else {
126
+ process.env.CREWX_DB = previousDbPath;
127
+ }
128
+ if (previousTaskId === undefined) {
129
+ delete process.env.CREWX_TASK_ID;
130
+ } else {
131
+ process.env.CREWX_TASK_ID = previousTaskId;
132
+ }
133
+ fs.rmSync(tempDir, { recursive: true, force: true });
134
+ }
135
+ });
72
136
  });
73
137
 
74
138
  describe('shared/skill-tracer output capture', () => {
@@ -85,6 +149,7 @@ describe('shared/skill-tracer output capture', () => {
85
149
  prevTaskId = process.env.CREWX_TASK_ID;
86
150
  process.env.CREWX_DB = dbPath;
87
151
  delete process.env.CREWX_TASK_ID;
152
+ createTracerDb(dbPath);
88
153
  });
89
154
 
90
155
  afterEach(() => {
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "rootDir": ".",
7
+ "outDir": ".",
8
+ "declaration": true,
9
+ "declarationMap": false,
10
+ "sourceMap": false,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": ["crewx-api-credential.ts"],
17
+ "exclude": ["node_modules", "tests"]
18
+ }