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.
@@ -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