archgraph-argo 0.26.2 → 0.27.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 +13 -4
- package/argo/.env.example +170 -163
- package/argo/plugins/argo-wakeup.js +29 -27
- package/argo/rules/archgraph.instructions.md +189 -187
- package/argo/schema/argob-rules.json +12229 -0
- package/argo/schema/argob.config.json +20 -0
- package/argo/scripts/argo-mcp-server.js +67 -1
- package/argo/scripts/argob-schema.js +574 -0
- package/argo/scripts/ensureArgoHarnessEnvironment.js +14 -0
- package/argo/scripts/external-graph-query.js +162 -0
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +0 -13
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +6 -61
- package/argo/scripts/graph-semantics.js +266 -227
- package/argo/scripts/runArchitectureTests.js +14 -6
- package/argo/scripts/systemarchitecture-mcp-server.js +87 -44
- package/argo/scripts/validateSystemArchitecture.js +260 -253
- package/argo/scripts/validator-mcp-server.js +1 -1
- package/argo/skills/argo-init/SKILL.md +7 -9
- package/argo/skills/ea-human-reconcile/SKILL.md +2 -1
- package/dsh-argo-wakeup/index.js +2 -2
- package/dsh-argo-workspace/index.js +3 -2
- package/install-argo.ps1 +215 -186
- package/package.json +5 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Cross-project graph query (federation client side).
|
|
4
|
+
//
|
|
5
|
+
// A read tool call may carry an optional `projectId`. Absent => the local
|
|
6
|
+
// workspace (unchanged). Present => the call is routed to the federation center
|
|
7
|
+
// (`POST <centerUrl>/graph/read`), which authorizes and forwards to the mirror
|
|
8
|
+
// engine; the native ARGO result is passed through with a `namespaceKey`. There
|
|
9
|
+
// is NO silent fallback to the local graph.
|
|
10
|
+
//
|
|
11
|
+
// The requester identity is the project's own `projectId`, read from
|
|
12
|
+
// `<workspace>/.argo/federation.json`. If it is missing, external queries fail
|
|
13
|
+
// with an explicit "register first" error — the id is never guessed.
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
const EXTERNAL_READ_TOOLS = new Set([
|
|
19
|
+
'getSystemArchitecture',
|
|
20
|
+
'getIntentElementContext',
|
|
21
|
+
'getArchitectureViewContext',
|
|
22
|
+
'queryNeo4jGraph',
|
|
23
|
+
'memory_search',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const FEDERATION_FILE = path.join('.argo', 'federation.json');
|
|
27
|
+
const DEFAULT_CENTER_URL = 'https://argo.derekworkspacev5.com';
|
|
28
|
+
|
|
29
|
+
function loadFederationIdentity(workspaceRoot) {
|
|
30
|
+
const file = path.join(workspaceRoot || process.cwd(), FEDERATION_FILE);
|
|
31
|
+
if (!fs.existsSync(file)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const identity = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
return identity && typeof identity === 'object' ? identity : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isExternalQuery(args) {
|
|
43
|
+
return Boolean(args) && typeof args.projectId === 'string' && args.projectId.trim() !== '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A tool call is external iff it is one of the readable tools AND carries projectId.
|
|
47
|
+
function externalQueryRequested(toolName, args) {
|
|
48
|
+
return EXTERNAL_READ_TOOLS.has(toolName) && isExternalQuery(args);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function forwardedArgs(args) {
|
|
52
|
+
const out = { ...(args || {}) };
|
|
53
|
+
// projectId is the router parameter; workspaceRoot/architecturePath are local-only.
|
|
54
|
+
delete out.projectId;
|
|
55
|
+
delete out.workspaceRoot;
|
|
56
|
+
delete out.architecturePath;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Route an external read. Returns the native ARGO result (with an added
|
|
62
|
+
* `namespaceKey`) on success, or `{ status:'failed', error:{...} }` — never the
|
|
63
|
+
* local result.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} options
|
|
66
|
+
* @param {string} options.workspaceRoot
|
|
67
|
+
* @param {string} options.tool
|
|
68
|
+
* @param {object} options.args
|
|
69
|
+
* @param {Function} [options.fetchImpl] - injectable fetch (tests)
|
|
70
|
+
*/
|
|
71
|
+
async function queryExternalRead({ workspaceRoot, tool, args, fetchImpl }) {
|
|
72
|
+
const fetchFn = fetchImpl || globalThis.fetch;
|
|
73
|
+
if (typeof fetchFn !== 'function') {
|
|
74
|
+
return { status: 'failed', error: { category: 'EXTERNAL_QUERY_UNREACHABLE', message: 'global fetch is unavailable in this runtime' } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const identity = loadFederationIdentity(workspaceRoot);
|
|
78
|
+
if (!identity || typeof identity.projectId !== 'string' || identity.projectId.trim() === '') {
|
|
79
|
+
return {
|
|
80
|
+
status: 'failed',
|
|
81
|
+
error: {
|
|
82
|
+
category: 'EXTERNAL_QUERY_NOT_REGISTERED',
|
|
83
|
+
message: `No federation identity at ${FEDERATION_FILE}. Register this project with the federation center first (registry_register) and write its projectId to ${FEDERATION_FILE}.`,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!EXTERNAL_READ_TOOLS.has(tool)) {
|
|
89
|
+
return {
|
|
90
|
+
status: 'failed',
|
|
91
|
+
error: { category: 'EXTERNAL_QUERY_TOOL_NOT_ALLOWED', message: `tool '${tool}' is not externally readable`, allowed: [...EXTERNAL_READ_TOOLS] },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const centerUrl = String(identity.centerUrl || DEFAULT_CENTER_URL).replace(/\/+$/, '');
|
|
96
|
+
const url = `${centerUrl}/graph/read`;
|
|
97
|
+
const body = {
|
|
98
|
+
requester: identity.projectId,
|
|
99
|
+
projectId: args.projectId,
|
|
100
|
+
tool,
|
|
101
|
+
args: forwardedArgs(args),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let response;
|
|
105
|
+
try {
|
|
106
|
+
response = await fetchFn(url, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'content-type': 'application/json' },
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
status: 'failed',
|
|
114
|
+
error: {
|
|
115
|
+
category: 'EXTERNAL_QUERY_UNREACHABLE',
|
|
116
|
+
message: `federation center unreachable: ${error && error.message ? error.message : error}`,
|
|
117
|
+
centerUrl,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let payload = null;
|
|
123
|
+
try { payload = await response.json(); } catch { /* leave null */ }
|
|
124
|
+
|
|
125
|
+
if (response.status === 200 && payload && payload.status === 'ok') {
|
|
126
|
+
// The engine returns the ARGO tool result; unwrap a {content:[{text}]}
|
|
127
|
+
// envelope so callers get the native payload, then stamp the namespaceKey.
|
|
128
|
+
let result = payload.result;
|
|
129
|
+
if (result && Array.isArray(result.content) && result.content[0] && typeof result.content[0].text === 'string') {
|
|
130
|
+
try { result = JSON.parse(result.content[0].text); } catch { /* keep the envelope */ }
|
|
131
|
+
}
|
|
132
|
+
if (result && typeof result === 'object') {
|
|
133
|
+
result = { ...result, namespaceKey: payload.namespaceKey || `proj:${args.projectId}` };
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const denied = Boolean(payload && payload.status === 'denied');
|
|
139
|
+
const reason = (payload && payload.reason)
|
|
140
|
+
|| (response.status === 403 ? 'not_authorized' : `http_${response.status}`);
|
|
141
|
+
return {
|
|
142
|
+
status: 'failed',
|
|
143
|
+
error: {
|
|
144
|
+
category: denied ? 'EXTERNAL_QUERY_DENIED' : 'EXTERNAL_QUERY_FAILED',
|
|
145
|
+
reason,
|
|
146
|
+
httpStatus: response.status,
|
|
147
|
+
requester: identity.projectId,
|
|
148
|
+
projectId: args.projectId,
|
|
149
|
+
namespaceKey: `proj:${args.projectId}`,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
EXTERNAL_READ_TOOLS,
|
|
156
|
+
FEDERATION_FILE,
|
|
157
|
+
DEFAULT_CENTER_URL,
|
|
158
|
+
loadFederationIdentity,
|
|
159
|
+
isExternalQuery,
|
|
160
|
+
externalQueryRequested,
|
|
161
|
+
queryExternalRead,
|
|
162
|
+
};
|
|
@@ -611,19 +611,6 @@ function assertProtectedSecretFile(expectedFilePath, adapters) {
|
|
|
611
611
|
) {
|
|
612
612
|
throw safeError('SECRET_FILE_GIT_STATE_UNSAFE');
|
|
613
613
|
}
|
|
614
|
-
const acl = adapters.acl && adapters.acl.inspect();
|
|
615
|
-
if (!acl || acl.status !== 0 || !acl.identity || typeof acl.stdout !== 'string') {
|
|
616
|
-
throw safeError('SECRET_FILE_ACL_UNVERIFIABLE');
|
|
617
|
-
}
|
|
618
|
-
const broadRead = acl.stdout
|
|
619
|
-
.split(/\r?\n/)
|
|
620
|
-
.some(line => /^(Everyone|BUILTIN\\Users|Authenticated Users):.*\((?:R|RX|F|M)\)/i.test(line.trim()));
|
|
621
|
-
const identityRead = acl.stdout
|
|
622
|
-
.split(/\r?\n/)
|
|
623
|
-
.some(line => line.trim().startsWith(`${acl.identity}:`) && /\((?:R|RX|F|M)\)/i.test(line));
|
|
624
|
-
if (broadRead || !identityRead) {
|
|
625
|
-
throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
626
|
-
}
|
|
627
614
|
}
|
|
628
615
|
|
|
629
616
|
function requireApprovedProfile(values) {
|
|
@@ -86,6 +86,8 @@ const LEGACY_KEYS = Object.freeze(['ARGO_NEO4J_URI', 'ARGO_NEO4J_USERNAME', 'ARG
|
|
|
86
86
|
const HOST_ONLY_ENV_KEYS = Object.freeze([
|
|
87
87
|
'ARGO_ENV_FILE',
|
|
88
88
|
'ARGO_REPO_ROOT',
|
|
89
|
+
'ARGO_SCHEMA_DIR',
|
|
90
|
+
'ARGO_ACTOR_ELEMENT_TYPE',
|
|
89
91
|
'ARGO_EA_QEA',
|
|
90
92
|
'ARGO_WORKSPACE_ROOTS',
|
|
91
93
|
'ARGO_SERVER_PATH',
|
|
@@ -97,6 +99,8 @@ const HOST_ONLY_ENV_KEYS = Object.freeze([
|
|
|
97
99
|
'ARGO_MCP_SEMANTIC_DEDUP',
|
|
98
100
|
'ARGO_MCP_SEMANTIC_DEDUP_THRESHOLD',
|
|
99
101
|
'ARGO_SEMANTIC_DEDUP_THRESHOLD',
|
|
102
|
+
'ARGO_COST_PROFILER',
|
|
103
|
+
'ARGO_COST_TRACE_MAX_BYTES',
|
|
100
104
|
]);
|
|
101
105
|
const PROHIBITED_RUNTIME_FIELD_KEYS = Object.freeze(['neo4jUri', 'embeddingCredential']);
|
|
102
106
|
const SECRET_KEYS = new Set(['ARGO_NEO4J_DATABASE_PASSWORD', 'QWEN_KEY', 'ARGO_RERANK_API_KEY', EMBEDDING_API_KEY_KEY]);
|
|
@@ -451,76 +455,18 @@ function preflightFile({ canonicalFilePath, configuredFilePath, filesystem, adap
|
|
|
451
455
|
|
|
452
456
|
let ignored;
|
|
453
457
|
let tracked;
|
|
454
|
-
let aclEvidence;
|
|
455
458
|
if (adapters.systemMetadata) {
|
|
456
459
|
const insideRepository = adapters.systemMetadata.isSecretFileInsideGitRepository().status === 0;
|
|
457
460
|
ignored = insideRepository ? adapters.systemMetadata.isSecretFileIgnored() : true;
|
|
458
461
|
tracked = insideRepository ? adapters.systemMetadata.isSecretFileTracked() : false;
|
|
459
|
-
if (process.platform === 'win32') {
|
|
460
|
-
const identityResult = adapters.systemMetadata.readCurrentIdentity();
|
|
461
|
-
const aclResult = adapters.systemMetadata.readSecretFileAcl();
|
|
462
|
-
aclEvidence = {
|
|
463
|
-
status: aclResult.status,
|
|
464
|
-
stdout: aclResult.stdout,
|
|
465
|
-
identity: identityResult.status === 0 ? identityResult.stdout.trim() : '',
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
462
|
} else {
|
|
469
463
|
ignored = adapters.git.isIgnored(configuredFilePath);
|
|
470
464
|
tracked = adapters.git.isTracked(configuredFilePath);
|
|
471
|
-
aclEvidence = adapters.acl.inspect(configuredFilePath);
|
|
472
465
|
}
|
|
466
|
+
// Only the "never committed" guarantee is enforced. OS-level ACL / file-mode
|
|
467
|
+
// hardening is intentionally NOT required (it caused frequent false failures).
|
|
473
468
|
if (tracked) throw safeError('SECRET_FILE_TRACKED');
|
|
474
469
|
if (!ignored) throw safeError('SECRET_FILE_NOT_IGNORED');
|
|
475
|
-
if (process.platform === 'win32') {
|
|
476
|
-
validateAcl(aclEvidence);
|
|
477
|
-
} else {
|
|
478
|
-
validatePosixSecretFileAcl(configuredFilePath, filesystem);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function validateAcl(result) {
|
|
483
|
-
if (!result || result.status !== 0 || typeof result.stdout !== 'string' || !result.identity) {
|
|
484
|
-
throw safeError('SECRET_FILE_ACL_UNVERIFIABLE');
|
|
485
|
-
}
|
|
486
|
-
const permissions = parseAcl(result.stdout);
|
|
487
|
-
const current = permissions.get(String(result.identity).toLowerCase());
|
|
488
|
-
if (!current || !current.allow || current.deny) throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
489
|
-
for (const broad of ['everyone', 'builtin\\users', 'authenticated users', 'nt authority\\authenticated users']) {
|
|
490
|
-
const permission = permissions.get(broad);
|
|
491
|
-
if (permission && permission.allow && !permission.deny) throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function parseAcl(output) {
|
|
496
|
-
const result = new Map();
|
|
497
|
-
for (const line of output.split(/\r?\n/)) {
|
|
498
|
-
const normalizedLine = line.trim().replace(/^[A-Za-z]:\\.*?\.env\s+/, '');
|
|
499
|
-
const match = normalizedLine.match(/^(.+?):((?:\([^)]*\))+)\s*$/);
|
|
500
|
-
if (!match) continue;
|
|
501
|
-
const tokens = [...match[2].matchAll(/\(([^)]*)\)/g)].map(item => item[1].toUpperCase());
|
|
502
|
-
if (!tokens.some(token => /^(?:F|M|R|RX)$/.test(token))) continue;
|
|
503
|
-
const key = match[1].trim().toLowerCase();
|
|
504
|
-
const entry = result.get(key) || { allow: false, deny: false };
|
|
505
|
-
if (tokens.includes('DENY')) entry.deny = true;
|
|
506
|
-
else entry.allow = true;
|
|
507
|
-
result.set(key, entry);
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function posixModeIsSecretSafe(mode) {
|
|
513
|
-
return (mode & 0o077) === 0 && (mode & 0o600) === 0o600;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function validatePosixSecretFileAcl(configuredFilePath, filesystem) {
|
|
517
|
-
const stat = filesystem.lstatSync(configuredFilePath);
|
|
518
|
-
if (!posixModeIsSecretSafe(stat.mode)) {
|
|
519
|
-
throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
520
|
-
}
|
|
521
|
-
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
522
|
-
throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
523
|
-
}
|
|
524
470
|
}
|
|
525
471
|
|
|
526
472
|
function productionSourceBehavior(repositoryRoot) {
|
|
@@ -612,7 +558,6 @@ function safeError(category) {
|
|
|
612
558
|
module.exports = {
|
|
613
559
|
resolveApprovedLiveConfiguration,
|
|
614
560
|
withApprovedLiveConfigurationTestComposition,
|
|
615
|
-
posixModeIsSecretSafe,
|
|
616
561
|
buildProfileConfiguration,
|
|
617
562
|
APPROVED_EMBEDDING_VALUES: APPROVED,
|
|
618
563
|
// The authoritative set of keys an approved `.env` file may carry. The
|