@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.
- package/crewx-api-credential.d.ts +14 -0
- package/crewx-api-credential.js +135 -0
- package/crewx-api-credential.ts +131 -0
- package/package.json +31 -2
- package/skill-tracer.js +46 -122
- package/tests/crewx-api-credential.test.ts +110 -0
- package/tests/skill-tracer.test.ts +65 -0
- package/tsconfig.json +18 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
if (!fs.existsSync(dir)) {
|
|
48
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
-
}
|
|
45
|
+
if (!fs.existsSync(dbPath)) return null;
|
|
50
46
|
|
|
51
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
125
|
-
|
|
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
|
|
62
|
+
db?.close();
|
|
162
63
|
} catch {
|
|
163
|
-
// Best-effort
|
|
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
|
+
}
|