@ran-sh/dsh-crew 0.3.8 → 0.4.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/README.md +17 -1
- package/README.zh.md +16 -1
- package/docs/gpt-relay-extension.md +103 -0
- package/docs/job-contracts.md +107 -0
- package/docs/readiness-matrix.md +73 -0
- package/official-web-bridge/lib/client.js +3446 -3446
- package/package.json +4 -1
- package/scripts/verify-official-bridge-e2e.mjs +34 -13
- package/src/extension-contract.mjs +78 -0
- package/src/failure-classification.mjs +29 -0
- package/src/hub/index.mjs +343 -62
- package/src/information-flow.mjs +67 -0
- package/src/install/npx-lifecycle.mjs +83 -3
- package/src/job-contracts.mjs +218 -0
- package/src/mcp-runtime.mjs +16 -8
- package/src/official-web-bridge.mjs +20 -5
- package/src/role-profiles.mjs +107 -0
- package/src/runtime-identity.mjs +6 -1
- package/src/server.mjs +141 -98
- package/src/workflow-runtime.mjs +109 -10
- package/src/workspace-context.mjs +146 -0
- package/src/workspace-readiness.mjs +32 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Workspace Context carries stable project facts by reference. Instruction
|
|
2
|
+
// file contents and validation output remain in the workspace and are never
|
|
3
|
+
// copied into this registry or across Agent hand-offs.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export const WORKSPACE_CONTEXT_SCHEMA_VERSION = 1;
|
|
10
|
+
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
11
|
+
|
|
12
|
+
export function isSafeBranchName(value) {
|
|
13
|
+
if (typeof value !== 'string' || value.length < 1 || value.length > 256) return false;
|
|
14
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(value)) return false;
|
|
15
|
+
if (value.includes('..') || value.includes('@{') || value.includes('//')) return false;
|
|
16
|
+
if (value.endsWith('/') || value.endsWith('.') || value.endsWith('.lock')) return false;
|
|
17
|
+
return value.split('/').every((part) => part && !part.startsWith('.'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function boundedStrings(value, { maxItems = 32, maxLength = 256 } = {}) {
|
|
21
|
+
if (value === undefined) return [];
|
|
22
|
+
if (!Array.isArray(value)) return null;
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
if (typeof item !== 'string' || item.trim() === '' || item.length > maxLength) return null;
|
|
26
|
+
result.push(item.trim());
|
|
27
|
+
if (result.length > maxItems) return null;
|
|
28
|
+
}
|
|
29
|
+
return [...new Set(result)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeReference(value) {
|
|
33
|
+
if (isAbsolute(value)) return false;
|
|
34
|
+
const normalized = value.replace(/\\/g, '/');
|
|
35
|
+
return normalized !== '..' && !normalized.startsWith('../') && !normalized.includes('/../');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeContext(id, raw) {
|
|
39
|
+
if (!ID.test(id) || !raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
40
|
+
if (typeof raw.repo_root !== 'string' || !isAbsolute(raw.repo_root)) return null;
|
|
41
|
+
const instructionFiles = boundedStrings(raw.instruction_files);
|
|
42
|
+
const validationHints = boundedStrings(raw.validation_hints, { maxItems: 32, maxLength: 512 });
|
|
43
|
+
if (!instructionFiles || !validationHints || instructionFiles.some((entry) => !safeReference(entry))) return null;
|
|
44
|
+
if (raw.default_branch != null && !isSafeBranchName(raw.default_branch)) return null;
|
|
45
|
+
return {
|
|
46
|
+
workspace_id: id,
|
|
47
|
+
repo_root: resolve(raw.repo_root),
|
|
48
|
+
default_branch: raw.default_branch?.trim() || null,
|
|
49
|
+
instruction_files: instructionFiles,
|
|
50
|
+
validation_hints: validationHints,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function workspaceContextsFile({ home = homedir() } = {}) {
|
|
55
|
+
return join(home, '.config', 'dsh-crew', 'workspaces.json');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loadWorkspaceContexts({ home = homedir(), file = workspaceContextsFile({ home }) } = {}) {
|
|
59
|
+
if (!existsSync(file)) {
|
|
60
|
+
return { schema_version: WORKSPACE_CONTEXT_SCHEMA_VERSION, ok: true, source: 'none', contexts: {}, errors: [] };
|
|
61
|
+
}
|
|
62
|
+
let raw;
|
|
63
|
+
try { raw = JSON.parse(readFileSync(file, 'utf8')); } catch {
|
|
64
|
+
return { schema_version: WORKSPACE_CONTEXT_SCHEMA_VERSION, ok: false, source: 'file', contexts: {}, errors: [{ code: 'WORKSPACE_CONTEXT_FILE_INVALID' }] };
|
|
65
|
+
}
|
|
66
|
+
return parseWorkspaceContexts(raw);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseWorkspaceContexts(raw) {
|
|
70
|
+
if (raw?.schema_version !== WORKSPACE_CONTEXT_SCHEMA_VERSION || !raw.workspaces || typeof raw.workspaces !== 'object' || Array.isArray(raw.workspaces)) {
|
|
71
|
+
return { schema_version: WORKSPACE_CONTEXT_SCHEMA_VERSION, ok: false, source: 'file', contexts: {}, errors: [{ code: 'WORKSPACE_CONTEXT_FILE_INVALID' }] };
|
|
72
|
+
}
|
|
73
|
+
const contexts = {};
|
|
74
|
+
const errors = [];
|
|
75
|
+
for (const [id, value] of Object.entries(raw.workspaces)) {
|
|
76
|
+
const context = normalizeContext(id, value);
|
|
77
|
+
if (!context) errors.push({ code: 'WORKSPACE_CONTEXT_INVALID', workspace_id: ID.test(id) ? id : '<invalid>' });
|
|
78
|
+
else contexts[id] = context;
|
|
79
|
+
}
|
|
80
|
+
return { schema_version: WORKSPACE_CONTEXT_SCHEMA_VERSION, ok: errors.length === 0, source: 'file', contexts, errors: errors.slice(0, 32) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function saveWorkspaceContexts(document, { home = homedir(), file = workspaceContextsFile({ home }) } = {}) {
|
|
84
|
+
const parsed = parseWorkspaceContexts(document);
|
|
85
|
+
if (!parsed.ok) return parsed;
|
|
86
|
+
const payload = { schema_version: WORKSPACE_CONTEXT_SCHEMA_VERSION, workspaces: parsed.contexts };
|
|
87
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
88
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
89
|
+
try {
|
|
90
|
+
writeFileSync(temp, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
91
|
+
renameSync(temp, file);
|
|
92
|
+
} catch {
|
|
93
|
+
rmSync(temp, { force: true });
|
|
94
|
+
return { ...parsed, ok: false, errors: [{ code: 'WORKSPACE_CONTEXT_FILE_WRITE_FAILED' }], error_code: 'WORKSPACE_CONTEXT_FILE_WRITE_FAILED' };
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function contains(root, target) {
|
|
100
|
+
const rel = relative(resolve(root), resolve(target));
|
|
101
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..\\`) && !rel.startsWith('../') && !isAbsolute(rel));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveWorkspaceContext(registry, { workspace_id: workspaceId, cwd } = {}) {
|
|
105
|
+
if (!workspaceId) return { ok: true, context: null };
|
|
106
|
+
const context = registry?.contexts?.[workspaceId];
|
|
107
|
+
if (!context) return { ok: false, code: 'WORKSPACE_CONTEXT_NOT_FOUND', workspace_id: workspaceId };
|
|
108
|
+
if (cwd && !contains(context.repo_root, cwd)) {
|
|
109
|
+
return { ok: false, code: 'WORKSPACE_ROOT_MISMATCH', workspace_id: workspaceId };
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, context: { ...context, instruction_files: [...context.instruction_files], validation_hints: [...context.validation_hints] } };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function buildWorkspaceTask(objective, context) {
|
|
115
|
+
const task = String(objective ?? '').slice(0, 32_768);
|
|
116
|
+
if (!context) return task;
|
|
117
|
+
const lines = [
|
|
118
|
+
'[DSH Workspace Context — references only]',
|
|
119
|
+
`Workspace: ${context.workspace_id}`,
|
|
120
|
+
`Repository root: ${context.repo_root}`,
|
|
121
|
+
];
|
|
122
|
+
if (context.default_branch) lines.push(`Default branch: ${context.default_branch}`);
|
|
123
|
+
if (context.instruction_files.length) lines.push(`Instruction references: ${context.instruction_files.join(', ')}`);
|
|
124
|
+
if (context.validation_hints.length) lines.push(`Validation hints: ${context.validation_hints.join(' | ')}`);
|
|
125
|
+
lines.push('Open referenced files in the workspace when needed; do not expect their contents in this hand-off.', '', '[Delegated objective]', task);
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function addContextReferences(context, references, { cwd } = {}) {
|
|
130
|
+
const refs = boundedStrings(references);
|
|
131
|
+
if (refs === null || refs.some((entry) => !safeReference(entry))) {
|
|
132
|
+
return { ok: false, code: 'WORKSPACE_CONTEXT_REFS_INVALID' };
|
|
133
|
+
}
|
|
134
|
+
if (refs.length === 0) return { ok: true, context };
|
|
135
|
+
const base = context ?? {
|
|
136
|
+
workspace_id: null,
|
|
137
|
+
repo_root: resolve(cwd ?? process.cwd()),
|
|
138
|
+
default_branch: null,
|
|
139
|
+
instruction_files: [],
|
|
140
|
+
validation_hints: [],
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
context: { ...base, instruction_files: [...new Set([...base.instruction_files, ...refs])] },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Read-only workspace preflight used by MCP and local HTTP readiness. It never
|
|
2
|
+
// changes branches, creates worktrees, or touches user files.
|
|
3
|
+
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { access as fsAccess } from 'node:fs/promises';
|
|
6
|
+
import { inspectRepository } from './workspace-isolation.mjs';
|
|
7
|
+
|
|
8
|
+
export async function assessWorkspaceReadiness({ cwd, inspect = inspectRepository, access = fsAccess } = {}) {
|
|
9
|
+
if (!cwd) return { ok: true, status: 'READY', reason_code: 'WORKSPACE_NOT_REQUESTED', repo_root: null, base_revision: null };
|
|
10
|
+
const repository = await inspect({ cwd });
|
|
11
|
+
if (!repository?.ok) {
|
|
12
|
+
return { ok: false, status: 'UNAVAILABLE', reason_code: repository?.reason ?? 'WORKSPACE_UNAVAILABLE' };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
await access(repository.repoRoot, constants.W_OK);
|
|
16
|
+
} catch {
|
|
17
|
+
return {
|
|
18
|
+
ok: true, status: 'READ_ONLY', reason_code: 'WORKSPACE_READ_ONLY',
|
|
19
|
+
repo_root: repository.repoRoot, base_revision: repository.baseRevision,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (repository.dirty === true) {
|
|
23
|
+
return {
|
|
24
|
+
ok: true, status: 'CONFLICT', reason_code: 'WORKSPACE_DIRTY',
|
|
25
|
+
repo_root: repository.repoRoot, base_revision: repository.baseRevision,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
ok: true, status: 'READY', reason_code: 'WORKSPACE_READY',
|
|
30
|
+
repo_root: repository.repoRoot, base_revision: repository.baseRevision,
|
|
31
|
+
};
|
|
32
|
+
}
|