@thebackstoryis/engineering-with-ai 0.3.0 → 0.3.1
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/Docs/README.md +2 -0
- package/Docs/context-management-and-token-efficiency.md +42 -0
- package/Docs/guide-catalogue.md +1 -0
- package/Docs/maintainers/contributing.md +8 -0
- package/Docs/operations/dashboard-configuration.md +22 -0
- package/Docs/operations/installation-updating-and-entitlements.md +55 -0
- package/Docs/reference/capabilities-and-project-layout.md +11 -6
- package/Docs/reference/cli-and-configuration.md +32 -0
- package/README.md +10 -0
- package/config/autonomy.schema.json +76 -11
- package/config/project.schema.json +29 -1
- package/package.json +3 -2
- package/public/autonomy.js +10 -4
- package/public/coding-providers.js +72 -0
- package/public/grok-credentials.js +42 -0
- package/public/index.html +36 -1
- package/public/styles.css +25 -0
- package/skills-src/ewai-context/SKILL.md +1 -0
- package/skills-src/ewai-deliver/SKILL.md +3 -1
- package/skills-src/ewai-pipeline/SKILL.md +1 -0
- package/src/autonomy.mjs +1 -1
- package/src/cli.mjs +69 -9
- package/src/coding-providers.mjs +81 -0
- package/src/companion.mjs +19 -4
- package/src/config-document.mjs +49 -0
- package/src/dashboard-preferences.mjs +7 -48
- package/src/delivery.mjs +8 -2
- package/src/discovery.mjs +5 -4
- package/src/grok-credentials.mjs +95 -0
- package/src/install.mjs +10 -4
- package/src/project.mjs +24 -2
- package/src/runtime/afk-conductor.mjs +84 -18
- package/src/runtime/autonomy-supervisor.mjs +23 -5
- package/src/runtime/autonomy-workers.mjs +11 -2
- package/src/runtime/dashboard-actions.mjs +1 -1
- package/src/runtime/dashboard-server.mjs +29 -0
- package/src/runtime/grok-provider.mjs +269 -0
- package/src/runtime/mcp-config.mjs +34 -2
- package/src/runtime/mcp-server.mjs +6 -6
- package/src/runtime/provider-adapters.mjs +207 -31
- package/src/validation-config.mjs +18 -7
- package/templates/SPECS/pipeline.yaml +3 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {constants,openSync,closeSync,fstatSync,lstatSync,readFileSync,writeFileSync,renameSync,unlinkSync,realpathSync,fsyncSync,existsSync} from 'node:fs';
|
|
2
|
+
import {dirname,relative,resolve,isAbsolute} from 'node:path';
|
|
3
|
+
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import {projectPaths} from './paths.mjs';
|
|
6
|
+
|
|
7
|
+
function fail(message,statusCode=409){const error=new Error(message);error.statusCode=statusCode;error.configurationError=true;throw error;}
|
|
8
|
+
function safePath(paths){
|
|
9
|
+
const root=realpathSync(paths.projectRoot),parent=dirname(paths.configPath),rel=relative(root,realpathSync(parent));
|
|
10
|
+
if(rel.startsWith('..')||isAbsolute(rel))fail('Settings require a safe project configuration path.',403);
|
|
11
|
+
let cursor=parent;
|
|
12
|
+
while(cursor!==paths.projectRoot){if(lstatSync(cursor).isSymbolicLink())fail('Settings cannot use a symbolic configuration directory.',403);const next=dirname(cursor);if(next===cursor)fail('Settings require a safe project configuration path.',403);cursor=next;}
|
|
13
|
+
const stat=lstatSync(paths.configPath);
|
|
14
|
+
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)fail('Settings require a regular, non-symbolic configuration file.',403);
|
|
15
|
+
if(stat.size>2*1024*1024)fail('The project configuration is too large for settings editing.',413);
|
|
16
|
+
return stat;
|
|
17
|
+
}
|
|
18
|
+
export function readProjectDocument(projectRoot){
|
|
19
|
+
const paths=projectPaths(projectRoot);let fd;
|
|
20
|
+
try{
|
|
21
|
+
const stat=safePath(paths);fd=openSync(paths.configPath,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
22
|
+
const opened=fstatSync(fd);if(opened.ino!==stat.ino||opened.dev!==stat.dev)fail('Project configuration changed. Reload the settings and try again.');
|
|
23
|
+
const bytes=readFileSync(fd);if(bytes.length>2*1024*1024)fail('The project configuration is too large for settings editing.',413);
|
|
24
|
+
const document=YAML.parseDocument(bytes.toString('utf8'));
|
|
25
|
+
if(document.errors.length)fail('Project configuration contains invalid YAML. Fix it before editing settings.',422);
|
|
26
|
+
let config;try{config=document.toJS({maxAliasCount:100});}catch{fail('The project configuration cannot be safely read.',422);}
|
|
27
|
+
if(config?.schema!=='ewai.project/v1'||config?.specs?.root!==paths.specsRelative)fail('Project configuration and SPECS locator must agree before editing settings.',422);
|
|
28
|
+
return {paths,document,config,digest:'sha256:'+createHash('sha256').update(bytes).digest('hex'),mode:stat.mode&0o777};
|
|
29
|
+
}catch(error){if(error.configurationError)throw error;fail('Project settings could not be read. Check configuration and file permissions.');}
|
|
30
|
+
finally{if(fd!==undefined)closeSync(fd);}
|
|
31
|
+
}
|
|
32
|
+
export function editProjectDocument(projectRoot,expectedDigest,apply){
|
|
33
|
+
if(!/^sha256:[a-f0-9]{64}$/.test(String(expectedDigest??'')))fail('Reload settings before saving.',400);
|
|
34
|
+
const paths=projectPaths(projectRoot),lock=paths.configPath+'.configuration.lock';let lockFd,tempFd,temporary;
|
|
35
|
+
try{
|
|
36
|
+
safePath(paths);
|
|
37
|
+
if(existsSync(paths.configPath+'.dashboard-preferences.lock'))fail('Settings are being edited. Retry after that save finishes.');
|
|
38
|
+
try{lockFd=openSync(lock,'wx',0o600);}catch{fail('Settings are being edited. Retry after that save finishes.');}
|
|
39
|
+
const before=readProjectDocument(projectRoot);if(before.digest!==expectedDigest)fail('Project configuration changed. Reload the settings and try again.');
|
|
40
|
+
apply(before.document,before.config);
|
|
41
|
+
const content=before.document.toString();
|
|
42
|
+
temporary=paths.configPath+'.settings-'+randomUUID()+'.tmp';tempFd=openSync(temporary,'wx',before.mode);
|
|
43
|
+
writeFileSync(tempFd,content,'utf8');fsyncSync(tempFd);closeSync(tempFd);tempFd=undefined;
|
|
44
|
+
if(readProjectDocument(projectRoot).digest!==before.digest)fail('Project configuration changed. Reload the settings and try again.');
|
|
45
|
+
safePath(paths);renameSync(temporary,paths.configPath);temporary=undefined;
|
|
46
|
+
return readProjectDocument(projectRoot);
|
|
47
|
+
}catch(error){if(error.configurationError||error.codingProviderError||error.dashboardPreferenceError)throw error;fail('Project settings could not be saved. Check configuration and file permissions.');}
|
|
48
|
+
finally{if(tempFd!==undefined)closeSync(tempFd);if(temporary)unlinkSync(temporary);if(lockFd!==undefined){closeSync(lockFd);unlinkSync(lock);}}
|
|
49
|
+
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {dirname,relative,resolve,isAbsolute} from 'node:path';
|
|
3
|
-
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
-
import YAML from 'yaml';
|
|
5
|
-
import {projectPaths} from './paths.mjs';
|
|
1
|
+
import {readProjectDocument,editProjectDocument} from './config-document.mjs';
|
|
6
2
|
|
|
7
3
|
export const DASHBOARD_VIEWS=Object.freeze([
|
|
8
4
|
['portfolio','Portfolio','Compare work and decisions across several projects.','Across projects and teams'],
|
|
@@ -27,54 +23,17 @@ export function normaliseDashboardPreferences(value={}){
|
|
|
27
23
|
return {sidebar_collapsed:value.sidebar_collapsed??false,optional_views:Object.fromEntries(DASHBOARD_VIEWS.map(v=>[v.id,optional[v.id]??false]))};
|
|
28
24
|
}
|
|
29
25
|
export function premiumPersonasActive(premium){return premium?.access==='available'&&premium?.installed===true&&premium?.verified===true;}
|
|
30
|
-
const
|
|
31
|
-
function
|
|
32
|
-
const root=realpathSync(paths.projectRoot),parent=dirname(paths.configPath),rel=relative(root,realpathSync(parent));
|
|
33
|
-
if(rel.startsWith('..')||isAbsolute(rel))fail('Dashboard preferences require a safe project configuration path.',403);
|
|
34
|
-
let cursor=parent;
|
|
35
|
-
while(cursor!==paths.projectRoot){if(lstatSync(cursor).isSymbolicLink())fail('Dashboard preferences cannot use a symbolic configuration directory.',403);const next=dirname(cursor);if(next===cursor)fail('Dashboard preferences require a safe project configuration path.',403);cursor=next;}
|
|
36
|
-
const stat=lstatSync(paths.configPath);
|
|
37
|
-
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)fail('Dashboard preferences require a regular, non-symbolic configuration file.',403);
|
|
38
|
-
if(stat.size>2*1024*1024)fail('The project configuration is too large for dashboard preference editing.',413);
|
|
39
|
-
return stat;
|
|
40
|
-
}
|
|
41
|
-
function snapshot(paths){
|
|
42
|
-
const stat=safePath(paths);let fd;
|
|
43
|
-
try{
|
|
44
|
-
fd=openSync(paths.configPath,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
45
|
-
const opened=fstatSync(fd);if(opened.ino!==stat.ino||opened.dev!==stat.dev)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
46
|
-
const bytes=readFileSync(fd);if(bytes.length>2*1024*1024)fail('The project configuration is too large for dashboard preference editing.',413);
|
|
47
|
-
const document=YAML.parseDocument(bytes.toString('utf8'));
|
|
48
|
-
if(document.errors.length)fail('The project configuration contains invalid YAML. Fix it before editing dashboard preferences.',422);
|
|
49
|
-
let config;try{config=document.toJS({maxAliasCount:100});}catch{fail('The project configuration cannot be safely read.',422);}
|
|
50
|
-
if(config?.schema!=='ewai.project/v1'||config?.specs?.root!==paths.specsRelative)fail('The project configuration and SPECS locator must agree before editing dashboard preferences.',422);
|
|
51
|
-
return {document,preferences:normaliseDashboardPreferences(config.dashboard),digest:digest(bytes),mode:stat.mode&0o777};
|
|
52
|
-
}finally{if(fd!==undefined)closeSync(fd);}
|
|
53
|
-
}
|
|
54
|
-
function guarded(operation){try{return operation();}catch(error){if(error.dashboardPreferenceError)throw error;fail('Dashboard preferences could not be read or saved. Check the project configuration and file permissions.',409);}}
|
|
26
|
+
function snapshot(projectRoot){const state=readProjectDocument(projectRoot);return {...state,preferences:normaliseDashboardPreferences(state.config.dashboard)};}
|
|
27
|
+
function guarded(operation){try{return operation();}catch(error){if(error.dashboardPreferenceError||error.configurationError)throw error;fail('Dashboard preferences could not be read or saved. Check project configuration and file permissions.',409);}}
|
|
55
28
|
function projection(state){return {schema:'ewai.dashboard-preferences/v1',digest:state.digest,preferences:state.preferences,views:DASHBOARD_VIEWS};}
|
|
56
|
-
export function readDashboardPreferences(projectRoot){return guarded(()=>projection(snapshot(
|
|
29
|
+
export function readDashboardPreferences(projectRoot){return guarded(()=>projection(snapshot(projectRoot)));}
|
|
57
30
|
export function saveDashboardPreferences(projectRoot,input){
|
|
58
31
|
return guarded(()=>{
|
|
59
32
|
keys(input,['confirmed','expectedDigest','preferences']);
|
|
60
33
|
if(input.confirmed!==true)fail('Confirm the dashboard preference changes before saving.');
|
|
61
|
-
if(!/^sha256:[a-f0-9]{64}$/.test(String(input.expectedDigest??'')))fail('Reload dashboard preferences before saving.');
|
|
62
34
|
if(!record(input.preferences))fail('Provide the dashboard preferences to save.');
|
|
63
|
-
const preferences=normaliseDashboardPreferences(input.preferences)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try{lockFd=openSync(lock,'wx',0o600);}catch{fail('Dashboard preferences are being edited. Retry after that save finishes.',409);}
|
|
67
|
-
const before=snapshot(paths);if(before.digest!==input.expectedDigest)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
68
|
-
before.document.set('dashboard',preferences);const content=before.document.toString();
|
|
69
|
-
temporary=paths.configPath+'.dashboard-'+randomUUID()+'.tmp';tempFd=openSync(temporary,'wx',before.mode);
|
|
70
|
-
writeFileSync(tempFd,content,'utf8');fsyncSync(tempFd);closeSync(tempFd);tempFd=undefined;
|
|
71
|
-
if(snapshot(paths).digest!==before.digest)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
72
|
-
safePath(paths);renameSync(temporary,paths.configPath);temporary=undefined;
|
|
73
|
-
return projection(snapshot(paths));
|
|
74
|
-
}finally{
|
|
75
|
-
if(tempFd!==undefined)closeSync(tempFd);
|
|
76
|
-
if(temporary)unlinkSync(temporary);
|
|
77
|
-
if(lockFd!==undefined){closeSync(lockFd);unlinkSync(lock);}
|
|
78
|
-
}
|
|
35
|
+
const preferences=normaliseDashboardPreferences(input.preferences);
|
|
36
|
+
const state=editProjectDocument(projectRoot,input.expectedDigest,document=>document.set('dashboard',preferences));
|
|
37
|
+
return projection({...state,preferences:normaliseDashboardPreferences(state.config.dashboard)});
|
|
79
38
|
});
|
|
80
39
|
}
|
package/src/delivery.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { dirname, relative, resolve } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import YAML from 'yaml';
|
|
13
|
+
import {codingPolicyDigest,assertCodingPolicyFresh,assertModelPolicy} from './coding-providers.mjs';
|
|
13
14
|
import { updateIntentDeliveryState, validateIntentStateCopies } from './intents.mjs';
|
|
14
15
|
import { loadProjectConfig } from './project.mjs';
|
|
15
16
|
import { readRuntimeIntent } from './runtime/intents.mjs';
|
|
@@ -38,7 +39,7 @@ import { validatePhaseArtefacts } from './delivery-artifacts.mjs';
|
|
|
38
39
|
import {
|
|
39
40
|
canonicalValidationProvider,
|
|
40
41
|
effectiveValidationConfig,
|
|
41
|
-
|
|
42
|
+
SUPPORTED_VALIDATION_PROVIDERS,
|
|
42
43
|
validationCheckpointForPhase,
|
|
43
44
|
} from './validation-config.mjs';
|
|
44
45
|
import {
|
|
@@ -431,7 +432,7 @@ function normaliseDeliveryValidation(paths, state) {
|
|
|
431
432
|
const legacyProviders = [...new Set(
|
|
432
433
|
(Array.isArray(state.providers) ? state.providers : [])
|
|
433
434
|
.map(canonicalValidationProvider)
|
|
434
|
-
.filter((provider) =>
|
|
435
|
+
.filter((provider) => SUPPORTED_VALIDATION_PROVIDERS.includes(provider)),
|
|
435
436
|
)];
|
|
436
437
|
|
|
437
438
|
for (const [checkpointName, checkpoint] of Object.entries(validation.checkpoints)) {
|
|
@@ -638,6 +639,10 @@ function recordOwnedExternalValidationCycle(projectRoot, slug, phaseId, input) {
|
|
|
638
639
|
const phase = findTrackedPhase(state, phaseId);
|
|
639
640
|
if (phase.status !== 'running') throw new Error(`Phase ${phaseId} is not running.`);
|
|
640
641
|
if (!phase.validation) throw new Error(`Phase ${phaseId} is not an external-validation checkpoint.`);
|
|
642
|
+
if(state.validation?.codingPolicyDigest){
|
|
643
|
+
const {config}=loadProjectConfig(projectRoot);
|
|
644
|
+
assertCodingPolicyFresh(state.validation.codingPolicyDigest,codingPolicyDigest(config.coding_providers));
|
|
645
|
+
}
|
|
641
646
|
|
|
642
647
|
const provider = String(input.provider ?? '').trim().toLowerCase();
|
|
643
648
|
if (!phase.validation.providers.includes(provider)) {
|
|
@@ -646,6 +651,7 @@ function recordOwnedExternalValidationCycle(projectRoot, slug, phaseId, input) {
|
|
|
646
651
|
);
|
|
647
652
|
}
|
|
648
653
|
const outcome = String(input.outcome ?? '').trim().toLowerCase();
|
|
654
|
+
assertModelPolicy(provider,state.validation?.codingPolicy);
|
|
649
655
|
if (!['pass', 'issues'].includes(outcome)) {
|
|
650
656
|
throw new Error('Validation cycle outcome must be pass or issues.');
|
|
651
657
|
}
|
package/src/discovery.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { loadProjectConfig } from './project.mjs';
|
|
|
20
20
|
import {
|
|
21
21
|
VALIDATION_CHECKPOINTS,
|
|
22
22
|
VALIDATION_PROVIDERS,
|
|
23
|
+
SUPPORTED_VALIDATION_PROVIDERS,
|
|
23
24
|
normaliseValidationConfig,
|
|
24
25
|
} from './validation-config.mjs';
|
|
25
26
|
import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
|
|
@@ -218,7 +219,7 @@ function normaliseOrganisationBlueprint(value) {
|
|
|
218
219
|
|
|
219
220
|
function discoveryValidation(raw, configured) {
|
|
220
221
|
const rawProviders = raw.providers ?? Object.fromEntries(
|
|
221
|
-
|
|
222
|
+
SUPPORTED_VALIDATION_PROVIDERS
|
|
222
223
|
.filter((provider) => raw[provider] !== undefined)
|
|
223
224
|
.map((provider) => [provider, raw[provider]]),
|
|
224
225
|
);
|
|
@@ -852,7 +853,7 @@ function renderDiscoveryEvidence(answers, detected, standards, compliance, compl
|
|
|
852
853
|
['AI features', answers.assurance.aiFeatures],
|
|
853
854
|
['Accessibility obligations', answers.assurance.accessibility],
|
|
854
855
|
['Availability', answers.assurance.availability],
|
|
855
|
-
...
|
|
856
|
+
...Object.keys(answers.validation.external.providers).map((provider) => [
|
|
856
857
|
`${provider[0].toUpperCase()}${provider.slice(1)} external validation`,
|
|
857
858
|
`${answers.validation.external.providers[provider].state}; ${
|
|
858
859
|
answers.validation.external.providers[provider].enabled ? 'enabled' : 'disabled'
|
|
@@ -879,7 +880,7 @@ function renderProjectConstraints(answers, completedAt) {
|
|
|
879
880
|
}
|
|
880
881
|
|
|
881
882
|
function renderApproach(answers, completedAt) {
|
|
882
|
-
const providerLines =
|
|
883
|
+
const providerLines = Object.keys(answers.validation.external.providers).map((provider) => {
|
|
883
884
|
const setting = answers.validation.external.providers[provider];
|
|
884
885
|
return `- **${provider[0].toUpperCase()}${provider.slice(1)}:** ${setting.state}; ${setting.enabled ? 'enabled' : 'disabled'}`;
|
|
885
886
|
}).join('\n');
|
|
@@ -1250,7 +1251,7 @@ export async function runDiscoveryInterview(projectRoot, io = {}) {
|
|
|
1250
1251
|
const availability = await askEnum(rl, 'Availability expectation', [...AVAILABILITY_LEVELS], 'standard');
|
|
1251
1252
|
const configuredProviders = config.validation?.external?.providers ?? {};
|
|
1252
1253
|
const validationProviders = {};
|
|
1253
|
-
for (const provider of VALIDATION_PROVIDERS) {
|
|
1254
|
+
for (const provider of [...VALIDATION_PROVIDERS,...(configuredProviders.grok?['grok']:[])]) {
|
|
1254
1255
|
const label = `${provider[0].toUpperCase()}${provider.slice(1)}`;
|
|
1255
1256
|
validationProviders[provider] = await askEnum(
|
|
1256
1257
|
rl,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {constants,openSync,closeSync,fstatSync,lstatSync,realpathSync,readFileSync,mkdirSync,writeFileSync,renameSync,unlinkSync,rmSync} from 'node:fs';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import {resolve,relative,isAbsolute} from 'node:path';
|
|
4
|
+
import {randomUUID,createHash} from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const messages={
|
|
7
|
+
'grok-credential-invalid':'Provide a valid xAI API key using the private setup form or hidden terminal prompt.',
|
|
8
|
+
'grok-credential-input':'Use the supported credential action and confirm the change.',
|
|
9
|
+
'grok-credential-unsafe-storage':'Private credential storage is unsafe or unavailable. Repair its permissions or use XAI_API_KEY in the launching environment.',
|
|
10
|
+
'grok-credential-invalid-storage':'The saved credential could not be read safely. Repair the private credential file before continuing.',
|
|
11
|
+
'grok-credential-stale':'The saved credential changed. Refresh its status and try again.',
|
|
12
|
+
'grok-credential-busy':'Another credential change is running. Retry after it finishes.',
|
|
13
|
+
'grok-credential-missing':'No Grok key is configured. Use private credential setup or XAI_API_KEY in the launching environment.',
|
|
14
|
+
'grok-key-rejected':'The key was not accepted. Check the key and its API permissions, then try again. The previous saved key was retained.',
|
|
15
|
+
'grok-check-rate-limited':'xAI limited the authentication check. Wait before trying again. The previous saved key was retained.',
|
|
16
|
+
'grok-check-unavailable':'The authentication check could not complete. Check your connection and try again. The previous saved key was retained.',
|
|
17
|
+
};
|
|
18
|
+
function fail(code,statusCode=400){const error=new Error(messages[code]??messages['grok-credential-input']);error.code=code;error.statusCode=statusCode;throw error;}
|
|
19
|
+
function key(value){if(typeof value!=='string'||value.length>512||!/^xai-[A-Za-z0-9_-]{16,508}$/.test(value.trim()))fail('grok-credential-invalid');return value.trim();}
|
|
20
|
+
function request(input,fields){if(!input||typeof input!=='object'||Array.isArray(input)||Object.keys(input).some(k=>!fields.includes(k))||input.confirmed!==true||typeof input.expectedRevision!=='string'||input.expectedRevision.length>80)fail('grok-credential-input');}
|
|
21
|
+
function stat(path){try{return lstatSync(path);}catch(error){if(error.code==='ENOENT')return null;fail('grok-credential-unsafe-storage');}}
|
|
22
|
+
function owned(s){return s&&!s.isSymbolicLink()&&(process.getuid===undefined||s.uid===process.getuid());}
|
|
23
|
+
function paths(options={},create=false){
|
|
24
|
+
const declaredHome=resolve(options.home??homedir());
|
|
25
|
+
const root=stat(declaredHome);if(!owned(root)||!root.isDirectory()||(root.mode&0o022))fail('grok-credential-unsafe-storage');
|
|
26
|
+
let home,project;try{home=realpathSync(declaredHome);if(options.projectRoot)project=realpathSync(resolve(options.projectRoot));}catch{fail('grok-credential-unsafe-storage');}
|
|
27
|
+
const folder=resolve(home,'.ewai/credentials'),file=resolve(folder,'grok.json');
|
|
28
|
+
if(project){const rel=relative(project,file);if(rel===''||(!isAbsolute(rel)&&rel!=='..'&&!rel.startsWith('../')))fail('grok-credential-unsafe-storage');}
|
|
29
|
+
const canonicalRoot=stat(home);if(!owned(canonicalRoot)||canonicalRoot.ino!==root.ino||canonicalRoot.dev!==root.dev)fail('grok-credential-unsafe-storage');
|
|
30
|
+
for(const path of [resolve(home,'.ewai'),folder]){
|
|
31
|
+
let s=stat(path);if(!s&&create){try{mkdirSync(path,{mode:0o700});}catch(error){if(error.code!=='EEXIST')fail('grok-credential-unsafe-storage');}s=stat(path);}
|
|
32
|
+
if(s&&(!owned(s)||!s.isDirectory()||(s.mode&(path===folder?0o077:0o022))))fail('grok-credential-unsafe-storage');
|
|
33
|
+
}
|
|
34
|
+
return {folder,file,location:JSON.stringify([home,root.dev,root.ino])};
|
|
35
|
+
}
|
|
36
|
+
function stored(options={}){
|
|
37
|
+
const {file,location}=paths(options),before=stat(file);if(!before)return {value:null,signature:'missing',location};
|
|
38
|
+
if(process.platform==='win32'||!owned(before)||!before.isFile()||before.nlink!==1||(before.mode&0o077)||before.size>4096)fail('grok-credential-unsafe-storage');
|
|
39
|
+
let fd;try{
|
|
40
|
+
fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW);const bytes=readFileSync(fd),after=fstatSync(fd),current=stat(file);
|
|
41
|
+
if(bytes.length>4096||!current||before.ino!==after.ino||before.dev!==after.dev||before.mtimeMs!==after.mtimeMs||after.ino!==current.ino||after.mtimeMs!==current.mtimeMs)fail('grok-credential-unsafe-storage');
|
|
42
|
+
let value;try{value=JSON.parse(bytes.toString('utf8'));}catch{fail('grok-credential-invalid-storage');}
|
|
43
|
+
if(value?.schema!=='ewai.grok-credential/v1'||typeof value.revision!=='string'||!/^[a-f0-9-]{36}$/.test(value.revision)||Object.keys(value).some(k=>!['schema','revision','apiKey'].includes(k)))fail('grok-credential-invalid-storage');
|
|
44
|
+
try{key(value.apiKey);}catch{fail('grok-credential-invalid-storage');}
|
|
45
|
+
return {value,location,signature:JSON.stringify([after.dev,after.ino,after.mtimeMs,after.ctimeMs,createHash('sha256').update(bytes).digest('hex')])};
|
|
46
|
+
}catch(error){if(error.code?.startsWith('grok-'))throw error;fail('grok-credential-unsafe-storage');}finally{if(fd!==undefined)closeSync(fd);}
|
|
47
|
+
}
|
|
48
|
+
function environment(options){const env=options.env??process.env;return env.XAI_API_KEY?key(env.XAI_API_KEY):null;}
|
|
49
|
+
export function grokCredentialStatus(options={}){
|
|
50
|
+
const {value}=stored(options),override=environment(options);
|
|
51
|
+
return {schema:'ewai.grok-credential-status/v1',provider:'grok',configured:Boolean(override||value),saved:Boolean(value),source:override?'environment':value?'saved':'missing',revision:value?.revision??'missing',storage:'owner-only-local-file',encrypted:false,storageSupported:process.platform!=='win32',authority:'credentials-only'};
|
|
52
|
+
}
|
|
53
|
+
export function resolveGrokApiKey(options={}){const override=environment(options);return override??stored(options).value?.apiKey??null;}
|
|
54
|
+
function predecessor(input,options,expected){const current=stored(options);if((current.value?.revision??'missing')!==input.expectedRevision||(expected&&(current.signature!==expected.signature||current.location!==expected.location)))fail('grok-credential-stale',409);return current;}
|
|
55
|
+
function mutation(options,operation){
|
|
56
|
+
if(process.platform==='win32')fail('grok-credential-unsafe-storage');
|
|
57
|
+
const {folder}=paths(options,true),lock=resolve(folder,'.grok.lock');
|
|
58
|
+
try{mkdirSync(lock,{mode:0o700});}catch(error){fail(error.code==='EEXIST'?'grok-credential-busy':'grok-credential-unsafe-storage',409);}
|
|
59
|
+
const identity=lstatSync(lock);
|
|
60
|
+
try{return operation();}finally{const current=stat(lock);if(owned(current)&¤t.isDirectory()&¤t.ino===identity.ino)rmSync(lock,{recursive:true});}
|
|
61
|
+
}
|
|
62
|
+
async function authenticate(apiKey,options={}){
|
|
63
|
+
const controller=new AbortController();let timer;
|
|
64
|
+
try{
|
|
65
|
+
const timeout=Math.min(10000,Math.max(10,options.timeoutMs??10000));
|
|
66
|
+
const response=await Promise.race([(options.fetchImpl??fetch)('https://api.x.ai/v1/models',{method:'GET',headers:{authorization:'Bearer '+apiKey},redirect:'manual',signal:controller.signal}),new Promise((_,reject)=>{timer=setTimeout(()=>{controller.abort();reject(Error('deadline'));},timeout);})]);
|
|
67
|
+
// Discard provider bodies, including errors. They are not safe evidence.
|
|
68
|
+
if(response.body)void response.body.cancel().catch(()=>{});
|
|
69
|
+
if(response.status===200)return {status:'verified',code:'grok-authentication-verified',message:'xAI authentication check passed. No output was generated; credit and coding readiness remain unverified.'};
|
|
70
|
+
const code=[401,403].includes(response.status)?'grok-key-rejected':response.status===429?'grok-check-rate-limited':'grok-check-unavailable';return {status:'failed',code,message:messages[code]};
|
|
71
|
+
}catch{return {status:'failed',code:'grok-check-unavailable',message:messages['grok-check-unavailable']};}
|
|
72
|
+
finally{clearTimeout(timer);controller.abort();}
|
|
73
|
+
}
|
|
74
|
+
export async function checkGrokConnection(options={}){
|
|
75
|
+
const apiKey=resolveGrokApiKey(options);
|
|
76
|
+
if(!apiKey)return {...grokCredentialStatus(options),status:'failed',code:'grok-credential-missing',message:messages['grok-credential-missing']};
|
|
77
|
+
return {...grokCredentialStatus(options),...await authenticate(apiKey,options)};
|
|
78
|
+
}
|
|
79
|
+
export async function configureGrokCredentials(input,options={}){
|
|
80
|
+
request(input,['confirmed','expectedRevision','apiKey']);const apiKey=key(input.apiKey);environment(options);const previous=predecessor(input,options);
|
|
81
|
+
const check=await authenticate(apiKey,options);if(check.status!=='verified')fail(check.code);
|
|
82
|
+
mutation(options,()=>{
|
|
83
|
+
predecessor(input,options,previous);const {file}=paths(options,true),temporary=file+'.'+randomUUID()+'.tmp';
|
|
84
|
+
try{writeFileSync(temporary,JSON.stringify({schema:'ewai.grok-credential/v1',revision:randomUUID(),apiKey})+'\n',{flag:'wx',mode:0o600});paths(options);predecessor(input,options,previous);renameSync(temporary,file);}
|
|
85
|
+
catch(error){if(error.code?.startsWith('grok-'))throw error;fail('grok-credential-unsafe-storage');}
|
|
86
|
+
finally{if(stat(temporary))unlinkSync(temporary);}
|
|
87
|
+
});
|
|
88
|
+
return {...grokCredentialStatus(options),check,message:'Grok key checked and saved for your account on this computer. No work started.'};
|
|
89
|
+
}
|
|
90
|
+
export function removeGrokCredentials(input,options={}){
|
|
91
|
+
request(input,['confirmed','expectedRevision']);environment(options);const previous=predecessor(input,options);
|
|
92
|
+
mutation(options,()=>{const current=predecessor(input,options,previous);if(current.value)unlinkSync(paths(options).file);});
|
|
93
|
+
const status=grokCredentialStatus(options);return {...status,message:status.source==='environment'?'Saved key removed. XAI_API_KEY still supplies the active credential.':'Saved Grok key removed. No work started.'};
|
|
94
|
+
}
|
|
95
|
+
export function safeGrokCredentialError(error){const code=Object.hasOwn(messages,error?.code)?error.code:'grok-credential-input';return {error:messages[code],code};}
|
package/src/install.mjs
CHANGED
|
@@ -31,11 +31,12 @@ function availableClaudeAgents() {
|
|
|
31
31
|
.sort();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function selectedHosts(host, home) {
|
|
34
|
+
function selectedHosts(host, home, grokHome) {
|
|
35
35
|
if (host !== 'auto') return [host];
|
|
36
36
|
const hosts = [];
|
|
37
37
|
if (existsSync(resolve(home, '.codex'))) hosts.push('codex');
|
|
38
38
|
if (existsSync(resolve(home, '.claude'))) hosts.push('claude');
|
|
39
|
+
if (existsSync(grokHome)) hosts.push('grok');
|
|
39
40
|
if (
|
|
40
41
|
existsSync(resolve(home, '.gemini/antigravity-cli'))
|
|
41
42
|
|| existsSync(resolve(home, '.gemini/config'))
|
|
@@ -43,13 +44,15 @@ function selectedHosts(host, home) {
|
|
|
43
44
|
return hosts.length ? hosts : ['codex'];
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function skillDestinationRoot({ scope, host, home, projectRoot }) {
|
|
47
|
+
function skillDestinationRoot({ scope, host, home, projectRoot, grokHome }) {
|
|
47
48
|
if (scope === 'project') {
|
|
48
49
|
if (!projectRoot) throw new Error('Project installation requires projectRoot');
|
|
50
|
+
if (host === 'grok') return resolve(projectRoot, '.grok/skills');
|
|
49
51
|
return host === 'claude'
|
|
50
52
|
? resolve(projectRoot, '.claude/skills')
|
|
51
53
|
: resolve(projectRoot, '.agents/skills');
|
|
52
54
|
}
|
|
55
|
+
if (host === 'grok') return resolve(grokHome, 'skills');
|
|
53
56
|
return host === 'claude'
|
|
54
57
|
? resolve(home, '.claude/skills')
|
|
55
58
|
: host === 'antigravity'
|
|
@@ -103,9 +106,11 @@ export function install(options = {}) {
|
|
|
103
106
|
if (!['global', 'project'].includes(scope)) throw new Error(`Unsupported install scope: ${scope}`);
|
|
104
107
|
if (!['copy', 'link'].includes(mode)) throw new Error(`Unsupported install mode: ${mode}`);
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
// Explicit fixture homes never inherit the real machine's GROK_HOME.
|
|
110
|
+
const grokHome=resolve(options.grokHome ?? (options.home===undefined ? (options.env??process.env).GROK_HOME : undefined) ?? resolve(home,'.grok'));
|
|
111
|
+
const hosts = selectedHosts(options.host ?? 'auto', home, grokHome);
|
|
107
112
|
for (const host of hosts) {
|
|
108
|
-
if (!['codex', 'claude', 'antigravity'].includes(host)) throw new Error(`Unsupported agent host: ${host}`);
|
|
113
|
+
if (!['codex', 'claude', 'antigravity', 'grok'].includes(host)) throw new Error(`Unsupported agent host: ${host}`);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
const results = [];
|
|
@@ -115,6 +120,7 @@ export function install(options = {}) {
|
|
|
115
120
|
scope,
|
|
116
121
|
host,
|
|
117
122
|
home,
|
|
123
|
+
grokHome,
|
|
118
124
|
projectRoot: options.projectRoot ? resolve(options.projectRoot) : ''
|
|
119
125
|
});
|
|
120
126
|
mkdirSync(destinationRoot, { recursive: true });
|
package/src/project.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, extname, relative, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
6
|
import {normaliseDashboardPreferences} from './dashboard-preferences.mjs';
|
|
7
|
+
import {normaliseCodingProviders} from './coding-providers.mjs';
|
|
7
8
|
import { projectPaths } from './paths.mjs';
|
|
8
9
|
import { premiumPersonaLearningAction } from './companion-opening.mjs';
|
|
9
10
|
import { initializeRuntime } from './runtime/database.mjs';
|
|
@@ -90,6 +91,20 @@ const checkinInstructionFiles = ['AGENTS.md', 'CLAUDE.md'];
|
|
|
90
91
|
const checkinInstruction = `<!-- EWAI-CHECKIN:START -->
|
|
91
92
|
## EWAI conversation check-in
|
|
92
93
|
|
|
94
|
+
### Concise, useful output
|
|
95
|
+
|
|
96
|
+
Prioritise correctness, fidelity and usefulness, then brevity. Resist verbosity: remove repetition, filler, restated requests and routine tool narration. Lead with the result or recommendation, using plain language and enough action detail to make it usable.
|
|
97
|
+
|
|
98
|
+
Preserve exact commands and identifiers, prerequisites, meaningful step order, failure evidence, uncertainty and human authority. Report only verification actually performed. Follow mandatory check-in, status, consent and phase protocols. Expand when a shorter answer would hide a consequential fact or when detail is requested; do not impose arbitrary word limits.
|
|
99
|
+
|
|
100
|
+
For a decision ask, include what needs deciding, relevant action details and consequences, supporting guidance, material options and tradeoffs, a recommendation with its reason, and a clear ask. Provide a suggested response when it helps the person act. If evidence is insufficient, recommend the next evidence-gathering step. Recommendations, defaults and suggested replies never constitute approval.
|
|
101
|
+
|
|
102
|
+
### Query refinement
|
|
103
|
+
|
|
104
|
+
Lead with the recommended interpretation or refined request, explaining consequential assumptions. Ask one focused question only when missing information materially changes the scope, outcome or next action; include supporting guidance and a recommended response with its reason. Keep uncertainty visible, preserve the user's intent and continue independent authorised work. After clarification, confirm the refined request briefly and act within its authority boundaries.
|
|
105
|
+
|
|
106
|
+
### Check-in and workflow
|
|
107
|
+
|
|
93
108
|
Keep the complete workflow, but present it cleanly: four routine status lines plus the returned menu at a decision point. Preserve warnings, blockers and unknown checks with short reasons; detailed diagnostics are on request. Progress is one sentence of at most 24 words per meaningful checkpoint. Do not narrate commands, file reads, JSON parsing or internal reasoning, and do not repeat the menu during a selected action. Never shorten briefing, purpose alignment, consent, standards or approval gates to meet an output limit. Use the guarded dashboard password form by default for licence setup; a private terminal prompt is an alternative only when a genuine interactive terminal is available. Never emulate hidden terminal entry through chat.
|
|
94
109
|
|
|
95
110
|
When \`.ewai-pipeline/project.json\` exists, perform one EWAI check-in at the start of each new agent conversation before substantive project work. That locator identifies the configured SPECS root; do not assume it is \`./SPECS\`. Claude may receive its check-in JSON from the managed SessionStart hook; interpret that result instead of running it twice. Otherwise run \`ewai checkin --project . --json\`. This starts or reuses the project-local pipeline dashboard and refreshes its SQLite projection. Always interpret and report the EWAI version/update status, premium entitlement and installed-library status, dashboard URL, state-integrity result, mandatory standards status, configured external validators, and any configured-versus-installed CLI mismatch. Show unknown checks with their reason rather than omitting them.
|
|
@@ -346,11 +361,11 @@ export function configureValidationCheckpoint(projectRoot, checkpoint, update) {
|
|
|
346
361
|
}
|
|
347
362
|
|
|
348
363
|
export function externalValidationStatus(config, orchestrator = 'manual') {
|
|
349
|
-
const effective =
|
|
364
|
+
const effective = validationStatus(config, orchestrator);
|
|
350
365
|
const selected = new Set(
|
|
351
366
|
Object.values(effective.checkpoints).flatMap((checkpoint) => checkpoint.validators),
|
|
352
367
|
);
|
|
353
|
-
return
|
|
368
|
+
return Object.keys(effective.providers).map((validator) => ({
|
|
354
369
|
validator,
|
|
355
370
|
state: effective.providers[validator].state,
|
|
356
371
|
enabled: effective.providers[validator].enabled,
|
|
@@ -360,6 +375,12 @@ export function externalValidationStatus(config, orchestrator = 'manual') {
|
|
|
360
375
|
}
|
|
361
376
|
|
|
362
377
|
export function validationStatus(config, orchestrator = 'manual') {
|
|
378
|
+
// Settings inspection has no coding-agent identity. Resolve independent
|
|
379
|
+
// roles only for an actual delivery host; phase dispatch still uses the
|
|
380
|
+
// strict effective configuration and cannot claim independence from manual.
|
|
381
|
+
if(orchestrator==='manual'&&config.coding_providers){
|
|
382
|
+
return {...effectiveValidationConfig(config.validation,orchestrator),roleResolution:'requires-orchestrator'};
|
|
383
|
+
}
|
|
363
384
|
return effectiveValidationConfig(config, orchestrator);
|
|
364
385
|
}
|
|
365
386
|
|
|
@@ -392,6 +413,7 @@ export function loadProjectConfig(projectRoot) {
|
|
|
392
413
|
}
|
|
393
414
|
config.validation = normaliseValidationConfig(config.validation);
|
|
394
415
|
config.dashboard = normaliseDashboardPreferences(config.dashboard);
|
|
416
|
+
if(config.coding_providers!==undefined)config.coding_providers=normaliseCodingProviders(config.coding_providers);
|
|
395
417
|
return { config, paths };
|
|
396
418
|
}
|
|
397
419
|
|