@thebackstoryis/engineering-with-ai 0.3.1-beta.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/context-management-and-token-efficiency.md +1 -1
- package/Docs/maintainers/contributing.md +8 -0
- package/Docs/operations/dashboard-configuration.md +22 -0
- package/Docs/operations/installation-updating-and-entitlements.md +40 -2
- package/Docs/reference/capabilities-and-project-layout.md +11 -6
- package/Docs/reference/cli-and-configuration.md +32 -0
- package/README.md +5 -10
- 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 +10 -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
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { accessSync, constants, createWriteStream, mkdirSync, existsSync, realpathSync, lstatSync, openSync, readSync, closeSync, fstatSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { delimiter, isAbsolute, resolve } from 'node:path';
|
|
1
|
+
import { accessSync, constants, createWriteStream, mkdirSync, existsSync, realpathSync, lstatSync, openSync, readSync, readFileSync, closeSync, fstatSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { delimiter, dirname, isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { release, tmpdir } from 'node:os';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { CONTEXT_ESTIMATE_METHOD, estimateContextTokens } from './context-assembly.mjs';
|
|
8
|
+
import {modelGuidance, readCodingPolicy, assertCodingPolicyFresh} from '../coding-providers.mjs';
|
|
9
|
+
import {prepareGrokRuntime, parseGrokWorkerResult, prepareGrokTaskSnapshot, acceptGrokTaskSnapshot, assertGrokTaskEvidenceFresh} from './grok-provider.mjs';
|
|
10
|
+
import {resolveGrokApiKey} from '../grok-credentials.mjs';
|
|
8
11
|
|
|
9
12
|
export const AFK_PROVIDERS = Object.freeze(['codex', 'claude', 'antigravity']);
|
|
13
|
+
export const SUPPORTED_AFK_PROVIDERS = Object.freeze([...AFK_PROVIDERS,'grok']);
|
|
14
|
+
const grokInvocations = new WeakMap();
|
|
10
15
|
// Retain owned child handles after an uncertain stop; do not turn a missing
|
|
11
16
|
// close event into success or lose the ability to observe a later exit.
|
|
12
17
|
const unsettledProcesses = new Set();
|
|
@@ -60,6 +65,15 @@ export function commandAvailable(command, env = process.env) {
|
|
|
60
65
|
});
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
export function captureProviderIdentity(provider) {
|
|
69
|
+
if(!SUPPORTED_AFK_PROVIDERS.includes(provider))throw new Error('Provider identity is unavailable.');
|
|
70
|
+
const executable=installedExecutable(providerCommand(provider));
|
|
71
|
+
const version=spawnSync(executable,['--version'],{env:phaseEnv(),encoding:'utf8',timeout:5000,maxBuffer:1024});
|
|
72
|
+
if(version.status!==0||version.error)throw new Error('Provider version is unavailable.');
|
|
73
|
+
return {provider,identityDigest:'sha256:'+createHash('sha256').update(JSON.stringify([
|
|
74
|
+
executableDigest(executable),version.stdout.trim(),release(),process.arch])).digest('hex')};
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
function claudeTools(task, mode) {
|
|
64
78
|
if (mode === 'review') return ['Read', 'Glob', 'Grep'];
|
|
65
79
|
const commands = (task?.allowed_commands ?? []).map((command) => `Bash(${command})`);
|
|
@@ -67,7 +81,7 @@ function claudeTools(task, mode) {
|
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
export function buildProviderInvocation(provider, input = {}) {
|
|
70
|
-
if (!
|
|
84
|
+
if (!SUPPORTED_AFK_PROVIDERS.includes(provider)) throw new Error(`Unsupported AFK provider: ${provider}`);
|
|
71
85
|
assertAfkMode(input.mode);
|
|
72
86
|
const mode = input.mode === 'review' ? 'review' : 'implementation';
|
|
73
87
|
const cwd = resolve(input.cwd);
|
|
@@ -76,16 +90,26 @@ export function buildProviderInvocation(provider, input = {}) {
|
|
|
76
90
|
provider,
|
|
77
91
|
command: providerCommand(provider),
|
|
78
92
|
cwd,
|
|
79
|
-
prompt: String(input.prompt ?? ''),
|
|
93
|
+
prompt: String(input.prompt ?? '')+modelGuidance(provider,input.codingPolicy),
|
|
80
94
|
timeoutMs: Number(input.timeoutMs ?? 45 * 60 * 1000),
|
|
81
95
|
mode,
|
|
82
96
|
task: input.task ?? null,
|
|
83
|
-
promptMeasurement: measureProviderPrompt(input.prompt ?? ''),
|
|
97
|
+
promptMeasurement: measureProviderPrompt(String(input.prompt ?? '')+modelGuidance(provider,input.codingPolicy)),
|
|
84
98
|
providerUsage: normaliseProviderUsage(provider, input.providerUsage),
|
|
85
99
|
args: [],
|
|
86
100
|
};
|
|
87
101
|
|
|
88
|
-
if (provider === '
|
|
102
|
+
if (provider === 'grok') {
|
|
103
|
+
if(model)throw new Error('Grok workers retain native model selection. Use an advisory model suggestion.');
|
|
104
|
+
assertGrokTaskEvidenceFresh(input.grokEvidence,mode,{...input,prompt:invocation.prompt});
|
|
105
|
+
const capability=prepareGrokBuildProvider(mode);
|
|
106
|
+
if(capability.status!=='requires-conformance')throw new Error('Grok coding/review mode is unavailable: '+capability.code);
|
|
107
|
+
const policyRoot=input.policyRoot??cwd,policyDigest=input.policyDigest??readCodingPolicy(policyRoot).policyDigest;
|
|
108
|
+
grokInvocations.set(invocation,{handle:capability,policyRoot,policyDigest,evidence:input.grokEvidence,mode,
|
|
109
|
+
input:{cwd,prompt:invocation.prompt,task:structuredClone(input.task),timeoutMs:invocation.timeoutMs}});
|
|
110
|
+
invocation.identityDigest=capability.identityDigest;
|
|
111
|
+
invocation.grokEvidence=input.grokEvidence;
|
|
112
|
+
} else if (provider === 'codex') {
|
|
89
113
|
invocation.args = ['--ask-for-approval', 'never', 'exec', '--color', 'never', '--sandbox', mode === 'review' ? 'read-only' : 'workspace-write', '-C', cwd];
|
|
90
114
|
if (mode === 'implementation') invocation.args.splice(3, 0, '--json');
|
|
91
115
|
if (model) invocation.args.push('--model', model);
|
|
@@ -121,6 +145,7 @@ export function invokeProvider(invocation, options = {}) {
|
|
|
121
145
|
if (options.signal?.aborted) return Promise.resolve({ provider: invocation.provider, exitCode: -1,
|
|
122
146
|
signal: '', cancelled: true, timedOut: false, executionStopped: true, processGroupStopped: true,
|
|
123
147
|
durationMs: 0, output: '', logPath: invocation.logPath, providerUsage: null });
|
|
148
|
+
if(invocation.provider==='grok')return invokeGrokBuild(invocation,options);
|
|
124
149
|
mkdirSync(resolve(invocation.logPath, '..'), { recursive: true });
|
|
125
150
|
const output = createWriteStream(invocation.logPath, { flags: 'a' });
|
|
126
151
|
const startedAt = Date.now();
|
|
@@ -220,7 +245,7 @@ function assertAfkMode(mode) {
|
|
|
220
245
|
// serialisable worker/browser supplied executable paths or permission flags.
|
|
221
246
|
const phaseCapabilities = new WeakMap();
|
|
222
247
|
const unavailablePhase = code => ({ status: 'unavailable', code, requiresHuman: true });
|
|
223
|
-
const phaseVersions = Object.freeze({ claude: '2.1.257 (Claude Code)', sandbox: 'macos-seatbelt-phase-v1' });
|
|
248
|
+
const phaseVersions = Object.freeze({ claude: '2.1.257 (Claude Code)', grok: 'grok 1.0.44 (5b807183dd79) [stable]', sandbox: 'macos-seatbelt-phase-v1' });
|
|
224
249
|
const phaseEnv = () => ({ PATH: process.env.PATH ?? '', LANG: 'en_US.UTF-8' });
|
|
225
250
|
|
|
226
251
|
function installedExecutable(command) {
|
|
@@ -249,41 +274,151 @@ function executableDigest(path) {
|
|
|
249
274
|
export function preparePhaseProvider(provider) {
|
|
250
275
|
// Additional providers/platforms stay unavailable until the same executable
|
|
251
276
|
// and configuration-isolation conformance has actually been demonstrated.
|
|
252
|
-
if (
|
|
277
|
+
if (!['claude', 'grok'].includes(provider) || process.platform !== 'darwin') return unavailablePhase('phase-provider-mode-unavailable');
|
|
253
278
|
try {
|
|
254
|
-
|
|
279
|
+
if (provider === 'grok' && ['managed_config.toml', 'requirements.toml'].some(name => existsSync(resolve('/etc/grok', name)))) return unavailablePhase('phase-provider-system-config-unverified');
|
|
280
|
+
const executable = installedExecutable(provider), sandbox = realpathSync('/usr/bin/sandbox-exec');
|
|
255
281
|
const version = path => {
|
|
256
282
|
const result = spawnSync(path, ['--version'], { env: phaseEnv(), encoding: 'utf8', timeout: 5000, maxBuffer: 1024 });
|
|
257
283
|
if (result.status !== 0 || result.error) throw new Error('phase-provider-version-unavailable');
|
|
258
284
|
return result.stdout.trim();
|
|
259
285
|
};
|
|
260
|
-
if (version(executable) !== phaseVersions
|
|
286
|
+
if (version(executable) !== phaseVersions[provider]) return unavailablePhase('phase-provider-version-unverified');
|
|
261
287
|
const hashes = [executableDigest(executable), executableDigest(sandbox)];
|
|
262
|
-
const handle = Object.freeze({ status: 'requires-conformance', provider, version: phaseVersions
|
|
288
|
+
const handle = Object.freeze({ status: 'requires-conformance', provider, version: phaseVersions[provider],
|
|
263
289
|
sandboxVersion: phaseVersions.sandbox,
|
|
264
290
|
identityDigest: createHash('sha256').update(JSON.stringify([hashes, release(), process.arch, phaseVersions.sandbox])).digest('hex') });
|
|
265
|
-
phaseCapabilities.set(handle, { executable, sandbox, hashes, verified: false });
|
|
291
|
+
phaseCapabilities.set(handle, { provider, mode:'phase', executable, sandbox, hashes, verified: false });
|
|
266
292
|
return handle;
|
|
267
293
|
} catch { return unavailablePhase('phase-provider-identity-unavailable'); }
|
|
268
294
|
}
|
|
269
295
|
|
|
296
|
+
export function prepareGrokBuildProvider(mode) {
|
|
297
|
+
if(!['implementation','review'].includes(mode))return unavailablePhase('phase-provider-mode-unavailable');
|
|
298
|
+
const phase=preparePhaseProvider('grok');if(phase.status!=='requires-conformance')return phase;
|
|
299
|
+
const handle=Object.freeze({...phase,mode,identityDigest:createHash('sha256').update(phase.identityDigest+':grok-task-snapshot-v1:'+mode).digest('hex')});
|
|
300
|
+
phaseCapabilities.set(handle,{...phaseCapabilities.get(phase),mode});return handle;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function invokeGrokBuild(invocation,options) {
|
|
304
|
+
const captured=grokInvocations.get(invocation),startedAt=Date.now();
|
|
305
|
+
if(!captured)throw new Error('Grok invocation must be prepared by the host.');
|
|
306
|
+
const failure=(code,stopped=true)=>({provider:'grok',exitCode:-1,status:'unavailable',code,
|
|
307
|
+
executionStopped:stopped,output:'',logPath:invocation.logPath,durationMs:Date.now()-startedAt});
|
|
308
|
+
const fresh=()=>{if(options.signal?.aborted)throw new Error('phase-cancelled');
|
|
309
|
+
currentPhaseCapability(captured.handle);assertCodingPolicyFresh(captured.policyDigest,readCodingPolicy(captured.policyRoot).policyDigest);
|
|
310
|
+
assertGrokTaskEvidenceFresh(captured.evidence,captured.mode,{...captured.input,policyRoot:captured.policyRoot});
|
|
311
|
+
options.beforeAccept?.();};
|
|
312
|
+
try {
|
|
313
|
+
assertGrokTaskEvidenceFresh(captured.evidence,captured.mode,{...captured.input,policyRoot:captured.policyRoot});
|
|
314
|
+
const proof=await verifyGrokBuildProviderConformance(captured.handle,{signal:options.signal,timeoutMs:Math.min(60000,captured.input.timeoutMs)});
|
|
315
|
+
if(proof.status!=='verified')return failure(proof.code,proof.executionStopped!==false);
|
|
316
|
+
const grokApiKey=resolveGrokApiKey();
|
|
317
|
+
if(!grokApiKey)return failure('phase-provider-credential-unavailable');
|
|
318
|
+
fresh();
|
|
319
|
+
const capability=currentPhaseCapability(captured.handle);
|
|
320
|
+
const result=await captureRestrictedProcess(capability,{...captured.input,grokApiKey,signal:options.signal,maxBytes:1024*1024,beforeAccept:fresh});
|
|
321
|
+
if(result.status!=='complete')return {...failure('phase-provider-'+result.status,result.executionStopped),status:result.status,
|
|
322
|
+
cancelled:result.status==='cancelled',timedOut:result.status==='timed-out'};
|
|
323
|
+
fresh();const parsed=parseGrokWorkerResult(result.output,capability.mode);
|
|
324
|
+
mkdirSync(resolve(invocation.logPath,'..'),{recursive:true});writeFileSync(invocation.logPath,parsed.output,{mode:0o600});
|
|
325
|
+
return {provider:'grok',...parsed,exitCode:parsed.status==='complete'?0:-1,executionStopped:true,
|
|
326
|
+
changedFiles:result.changedFiles??[],promptMeasurement:invocation.promptMeasurement,
|
|
327
|
+
logPath:invocation.logPath,durationMs:Date.now()-startedAt};
|
|
328
|
+
}catch(error){return failure(error.code?.startsWith('grok-credential-')?error.code:error.message.startsWith('grok-task-evidence-')||['phase-cancelled','phase-provider-identity-changed'].includes(error.message)?error.message:'phase-provider-acceptance-failed');}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function verifyGrokBuildProviderConformance(handle, control={}) {
|
|
332
|
+
let capability;
|
|
333
|
+
try{capability=currentPhaseCapability(handle,false);}catch{return unavailablePhase('phase-provider-conformance-required');}
|
|
334
|
+
if(capability.provider!=='grok'||!['implementation','review'].includes(capability.mode))return unavailablePhase('phase-provider-mode-unavailable');
|
|
335
|
+
capability.verified=false;
|
|
336
|
+
const phaseHandle=Object.freeze({...handle,mode:'phase'});
|
|
337
|
+
phaseCapabilities.set(phaseHandle,{...capability,mode:'phase',verified:false});
|
|
338
|
+
const isolation=await verifyPhaseProviderConformance(phaseHandle,control);
|
|
339
|
+
if(isolation.status!=='verified')return isolation;
|
|
340
|
+
const root=realpathSync(mkdtempSync(resolve(tmpdir(),'ewai-grok-build-conformance-'))),project=resolve(root,'project');
|
|
341
|
+
mkdirSync(project);writeFileSync(resolve(project,'code.txt'),'before');
|
|
342
|
+
const outside=resolve(root,'canonical-approval.json'),requests=[];
|
|
343
|
+
let attack=false,executionStopped=true;
|
|
344
|
+
const server=createServer((request,response)=>{
|
|
345
|
+
if(request.method==='GET'){requests.push({health:['/','/v1','/v1/models'].includes(request.url)});
|
|
346
|
+
response.writeHead(200,{'content-type':'application/json'});response.end(JSON.stringify({data:[{id:'fixture',object:'model'}]}));return;}
|
|
347
|
+
let body='';request.on('data',chunk=>{body+=chunk;if(Buffer.byteLength(body)>2*1024*1024)request.destroy();});
|
|
348
|
+
request.on('end',()=>{
|
|
349
|
+
let value;try{value=JSON.parse(body);}catch{response.writeHead(400);response.end();return;}
|
|
350
|
+
const tools=(value.tools??[]).map(tool=>tool.function?.name??tool.name);
|
|
351
|
+
requests.push({health:false,route:request.url,tools,canary:body.includes('EWAI_UNTRUSTED_INSTRUCTIONS_CANARY'),
|
|
352
|
+
readObserved:!attack&&(value.messages??[]).some(message=>message.role==='tool'&&JSON.stringify(message.content).includes('before'))});
|
|
353
|
+
const title=tools.length===1&&tools[0]==='session_title';
|
|
354
|
+
const finished=(value.messages??[]).some(message=>message.role==='tool');
|
|
355
|
+
const call=!title&&!finished&&tools.length>1;
|
|
356
|
+
const read=capability.mode==='review'&&!attack;
|
|
357
|
+
const delta=call?{tool_calls:[{index:0,id:'fixture-tool',type:'function',function:{name:read?'read_file':'write',
|
|
358
|
+
arguments:JSON.stringify(read?{target_file:'code.txt'}:{file_path:attack?outside:'code.txt',content:'after'})}}]}:{role:'assistant',content:'EWAI_BUILD_FIXTURE_ONLY'};
|
|
359
|
+
response.writeHead(200,{'content-type':'text/event-stream'});
|
|
360
|
+
const event=data=>response.write('data: '+JSON.stringify(data)+'\n\n');
|
|
361
|
+
event({id:'fixture',object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta,finish_reason:null}]});
|
|
362
|
+
event({id:'fixture',object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta:{},finish_reason:call?'tool_calls':'stop'}],usage:{prompt_tokens:1,completion_tokens:1,total_tokens:2}});
|
|
363
|
+
response.end('data: [DONE]\n\n');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
try{
|
|
367
|
+
await new Promise((ok,no)=>{server.once('error',no);server.listen(0,'127.0.0.1',ok);});
|
|
368
|
+
const fixture={url:'http://127.0.0.1:'+server.address().port,isolationProbe:true,
|
|
369
|
+
taskInput:{cwd:project,task:{read_set:['code.txt'],write_set:['code.txt']}}};
|
|
370
|
+
const input={prompt:'Use only declared task files. Return the fixture token.',timeoutMs:15000,maxBytes:256*1024,signal:control.signal};
|
|
371
|
+
const normal=await captureRestrictedProcess(capability,input,fixture);executionStopped&&=normal.executionStopped;
|
|
372
|
+
const after=readFileSync(resolve(project,'code.txt'),'utf8');
|
|
373
|
+
attack=true;
|
|
374
|
+
const forged=await captureRestrictedProcess(capability,input,fixture);executionStopped&&=forged.executionStopped;
|
|
375
|
+
const expected=capability.mode==='implementation'?['read_file','search_replace','list_dir','grep','write']:['read_file','list_dir','grep'];
|
|
376
|
+
const checks={
|
|
377
|
+
...Object.fromEntries(['canonicalWritePrevented','outputBoundEnforced','timeoutEnforced','networkIsolated','childProcessPrevented'].map(key=>[key,isolation[key]===true])),
|
|
378
|
+
nativeResult:normal.status==='complete'&&parseGrokWorkerResult(normal.output,capability.mode).output==='EWAI_BUILD_FIXTURE_ONLY',
|
|
379
|
+
declaredWriteBoundary:capability.mode==='implementation'?after==='after'&&normal.changedFiles?.join(',')==='code.txt':after==='before'&&!normal.changedFiles?.length,
|
|
380
|
+
nativeReadBoundary:capability.mode!=='review'||requests.some(request=>request.readObserved),
|
|
381
|
+
inheritedCapabilitiesBlocked:requests.every(r=>r.health===true||r.route==='/v1/chat/completions'&&!r.canary
|
|
382
|
+
&&(r.tools.length===1&&r.tools[0]==='session_title'||r.tools.length===0||r.tools.length===expected.length&&r.tools.every(tool=>expected.includes(tool)))),
|
|
383
|
+
forbiddenWritePrevented:forged.executionStopped&&!existsSync(outside)&&readFileSync(resolve(project,'code.txt'),'utf8')===after,
|
|
384
|
+
executionStopped,
|
|
385
|
+
};
|
|
386
|
+
capability.verified=Object.values(checks).every(value=>value===true);currentPhaseCapability(handle,false);
|
|
387
|
+
return capability.verified?{status:'verified',provider:'grok',mode:capability.mode,identityDigest:handle.identityDigest,
|
|
388
|
+
checks,fixtureServiceOnly:true,authority:'none'}:{...unavailablePhase('phase-provider-build-conformance-failed'),checks,
|
|
389
|
+
requests:requests.map(r=>({route:r.health?'fixture-health':'fixture-model',tools:r.tools??[],inheritedInstructions:r.canary===true}))};
|
|
390
|
+
}catch(error){return {...unavailablePhase('phase-provider-build-conformance-failed'),executionStopped,
|
|
391
|
+
diagnosticCode:/^grok-task-[a-z-]+$/.test(error.message)?error.message:'native-fixture-failed'};}
|
|
392
|
+
finally{server.closeAllConnections();await new Promise(ok=>server.close(ok));if(executionStopped)rmSync(root,{recursive:true,force:true});}
|
|
393
|
+
}
|
|
394
|
+
|
|
270
395
|
function currentPhaseCapability(handle, requireVerified = true) {
|
|
271
396
|
const capability = phaseCapabilities.get(handle);
|
|
272
397
|
if (!capability || requireVerified && !capability.verified) throw new Error('phase-provider-conformance-required');
|
|
398
|
+
if (installedExecutable(capability.provider) !== capability.executable
|
|
399
|
+
|| capability.provider === 'grok' && ['managed_config.toml', 'requirements.toml'].some(name => existsSync(resolve('/etc/grok', name)))) {
|
|
400
|
+
capability.verified = false; throw new Error('phase-provider-identity-changed');
|
|
401
|
+
}
|
|
273
402
|
if (executableDigest(capability.executable) !== capability.hashes[0] || executableDigest(capability.sandbox) !== capability.hashes[1]) {
|
|
274
403
|
capability.verified = false; throw new Error('phase-provider-identity-changed');
|
|
275
404
|
}
|
|
276
405
|
return capability;
|
|
277
406
|
}
|
|
278
407
|
|
|
279
|
-
function restrictedPhaseArgs(runtime, command, args, fixture) {
|
|
408
|
+
function restrictedPhaseArgs(runtime, command, args, fixture, grok=null, mode='phase') {
|
|
280
409
|
// One kernel policy covers every restriction; nested Seatbelt initialisation
|
|
281
410
|
// is not supported. Denying fork also denies posix_spawn, so a child cannot
|
|
282
411
|
// detach into a new session and outlive the captured process.
|
|
283
412
|
const port = fixture?.url ? Number(new URL(fixture.url).port) : null;
|
|
413
|
+
const ancestorDirectories=[];
|
|
414
|
+
for(const start of [runtime,realpathSync(command)]) for(let path=dirname(start);path!==dirname(path);path=dirname(path))ancestorDirectories.push(path);
|
|
284
415
|
const policy = `(version 1)(allow default)(deny file-write*)`
|
|
285
416
|
+ `(allow file-write* (subpath ${JSON.stringify(runtime)}))(deny process-fork)`
|
|
286
|
-
+ (fixture ? `(deny network*)${port ? `(allow network-outbound (remote ip "localhost:${port}"))` : ''}` : '')
|
|
417
|
+
+ (fixture ? `(deny network*)${port ? `(allow network-outbound (remote ip "localhost:${port}"))` : ''}` : '')
|
|
418
|
+
+ (grok ? `(deny file-read-data)(allow file-read-data (literal "/") ${ancestorDirectories.map(path=>`(literal ${JSON.stringify(path)})`).join(' ')} (subpath "/System") (subpath "/usr") (subpath "/Library") (subpath "/dev") (subpath "/opt/homebrew/Cellar") (subpath "/opt/homebrew/lib") (subpath "/opt/homebrew/opt") (literal ${JSON.stringify(realpathSync(command))}) (subpath ${JSON.stringify(runtime)}))`
|
|
419
|
+
+ `(deny file-write* (literal ${JSON.stringify(resolve(grok.home,'.grok/config.toml'))}) (literal ${JSON.stringify(resolve(grok.home,'.grok/managed_config.toml'))}) (literal ${JSON.stringify(resolve(grok.home,'.grok/requirements.toml'))}))`
|
|
420
|
+
+ ['.grok','.claude','.cursor','.git','.ewai-pipeline'].map(name=>`(deny file-write* (subpath ${JSON.stringify(resolve(grok.cwd,name))}))`).join('')
|
|
421
|
+
+ (mode==='review'?`(deny file-write* (subpath ${JSON.stringify(grok.cwd)}))`:'') : '');
|
|
287
422
|
return ['-p', policy, command, ...args];
|
|
288
423
|
}
|
|
289
424
|
|
|
@@ -298,6 +433,8 @@ async function captureRestrictedProcess(capability, input, fixture = null) {
|
|
|
298
433
|
if (input.signal?.aborted) return cancelledBeforeSpawn();
|
|
299
434
|
const directory = realpathSync(mkdtempSync(resolve(tmpdir(), 'ewai-phase-process-')));
|
|
300
435
|
const identity = lstatSync(directory), runtime = resolve(directory, 'runtime');
|
|
436
|
+
let stopped = true;
|
|
437
|
+
try {
|
|
301
438
|
mkdirSync(runtime, { mode: 0o700 });
|
|
302
439
|
if (fixture?.isolationProbe) {
|
|
303
440
|
// Deliberately hostile *fixture* configuration. A loaded hook/MCP server
|
|
@@ -309,16 +446,20 @@ async function captureRestrictedProcess(capability, input, fixture = null) {
|
|
|
309
446
|
writeFileSync(resolve(directory, '.mcp.json'), JSON.stringify({ mcpServers: { forbidden: { type: 'http', url: `${fixture.url}/forbidden-mcp` } } }), { mode: 0o600 });
|
|
310
447
|
writeFileSync(resolve(directory, 'CLAUDE.md'), 'EWAI_UNTRUSTED_INSTRUCTIONS_CANARY', { mode: 0o600 });
|
|
311
448
|
}
|
|
312
|
-
const
|
|
449
|
+
const mode=capability.mode??'phase';
|
|
450
|
+
const grok = capability.provider === 'grok' ? prepareGrokRuntime(directory, runtime, input.prompt, fixture, mode, {...process.env,XAI_API_KEY:input.grokApiKey}) : null;
|
|
451
|
+
const taskInput=fixture?.taskInput??input;
|
|
452
|
+
const snapshot=grok&&mode!=='phase'&&!fixture?.command
|
|
453
|
+
?prepareGrokTaskSnapshot(taskInput.cwd,grok.cwd,taskInput.task):null;
|
|
454
|
+
const env = grok?.env ?? { ...phaseEnv(), CLAUDE_CONFIG_DIR: runtime, CLAUDE_CODE_TMPDIR: runtime, TMPDIR: runtime,
|
|
313
455
|
CLAUDE_CODE_SKIP_PROMPT_HISTORY: '1', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
314
456
|
DISABLE_TELEMETRY: '1', DISABLE_ERROR_REPORTING: '1',
|
|
315
457
|
ANTHROPIC_API_KEY: fixture ? 'ewai-fixture-not-a-real-key' : process.env.ANTHROPIC_API_KEY };
|
|
316
458
|
if (fixture?.url) env.ANTHROPIC_BASE_URL = fixture.url;
|
|
317
|
-
|
|
459
|
+
if (grok && fixture?.command === process.execPath) env.OPENSSL_CONF = '/dev/null';
|
|
460
|
+
const command = fixture?.command ?? capability.executable, args = fixture?.args ?? grok?.args ?? restrictedClaudeArgs();
|
|
318
461
|
const launchCommand = capability.sandbox;
|
|
319
|
-
const launchArgs = restrictedPhaseArgs(runtime, command, args, fixture);
|
|
320
|
-
let stopped = false;
|
|
321
|
-
try {
|
|
462
|
+
const launchArgs = restrictedPhaseArgs(runtime, command, args, fixture, grok, mode);
|
|
322
463
|
const result = await new Promise(resolvePromise => {
|
|
323
464
|
let child, timer, killTimer, stopDeadline, terminal = null, bytes = 0, output = [], settled = false;
|
|
324
465
|
const kill = signal => { if (child?.pid) try { process.kill(-child.pid, signal); } catch {} };
|
|
@@ -359,7 +500,7 @@ async function captureRestrictedProcess(capability, input, fixture = null) {
|
|
|
359
500
|
output: !terminal && stopped ? Buffer.concat(output).toString('utf8') : '', bytes });
|
|
360
501
|
};
|
|
361
502
|
if (input.signal?.aborted) { stopped = true; resolvePromise(cancelledBeforeSpawn()); return; }
|
|
362
|
-
try { child = spawn(launchCommand, launchArgs, { cwd: directory, env, detached: true, shell: false, stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
503
|
+
try { stopped=false; child = spawn(launchCommand, launchArgs, { cwd: grok?.cwd ?? directory, env, detached: true, shell: false, stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
363
504
|
catch { finish(-1, null); return; }
|
|
364
505
|
const capture = (chunk, stdout) => {
|
|
365
506
|
bytes += chunk.length;
|
|
@@ -372,8 +513,11 @@ async function captureRestrictedProcess(capability, input, fixture = null) {
|
|
|
372
513
|
timer = setTimeout(() => stop('timed-out'), input.timeoutMs);
|
|
373
514
|
input.signal?.addEventListener('abort', cancel, { once: true });
|
|
374
515
|
if (input.signal?.aborted) cancel();
|
|
375
|
-
child.stdin.end(terminal ? '' : input.prompt);
|
|
516
|
+
child.stdin.end(terminal || grok ? '' : input.prompt);
|
|
376
517
|
});
|
|
518
|
+
if(snapshot&&result.status==='complete'&&parseGrokWorkerResult(result.output,mode).status==='complete'){
|
|
519
|
+
result.changedFiles=acceptGrokTaskSnapshot(snapshot,{readOnly:mode==='review',beforeAccept:input.beforeAccept});
|
|
520
|
+
}
|
|
377
521
|
return result;
|
|
378
522
|
} finally {
|
|
379
523
|
if (stopped && existsSync(directory)) {
|
|
@@ -389,9 +533,15 @@ export async function invokeRestrictedPhaseProvider(handle, input = {}) {
|
|
|
389
533
|
|| input.signal !== undefined && !(input.signal instanceof AbortSignal)) return unavailablePhase('phase-provider-input-invalid');
|
|
390
534
|
let capability;
|
|
391
535
|
try { capability = currentPhaseCapability(handle); } catch { return unavailablePhase('phase-provider-conformance-required'); }
|
|
392
|
-
if
|
|
393
|
-
|
|
536
|
+
if(capability.mode!=='phase')return unavailablePhase('phase-provider-mode-unavailable');
|
|
537
|
+
if (input.signal?.aborted) return { status: 'cancelled', exitCode: -1, executionStopped: true, spawned: false, output: '' };
|
|
538
|
+
let apiKey;
|
|
539
|
+
try{apiKey=capability.provider==='grok'?resolveGrokApiKey():process.env.ANTHROPIC_API_KEY;}catch{return unavailablePhase('phase-provider-credential-unavailable');}
|
|
540
|
+
if(!apiKey)return unavailablePhase('phase-provider-credential-unavailable');
|
|
541
|
+
const captured = await captureRestrictedProcess(capability, { ...input, grokApiKey:capability.provider==='grok'?apiKey:undefined, maxBytes: 256 * 1024 });
|
|
394
542
|
if (captured.status !== 'complete') return { ...captured, output: '' };
|
|
543
|
+
try { currentPhaseCapability(handle); } catch { return { ...unavailablePhase('phase-provider-identity-changed'), executionStopped: true, output: '' }; }
|
|
544
|
+
if (capability.provider === 'grok') return { ...parseGrokWorkerResult(captured.output), exitCode: captured.exitCode, executionStopped: true };
|
|
395
545
|
let envelope;
|
|
396
546
|
try { envelope = JSON.parse(captured.output); } catch { return { status: 'malformed', executionStopped: true, exitCode: 0, output: '' }; }
|
|
397
547
|
if (envelope?.type !== 'result' || envelope.subtype !== 'success' || envelope.is_error !== false || typeof envelope.result !== 'string') {
|
|
@@ -429,6 +579,10 @@ export async function verifyPhaseProviderConformance(handle, control = {}) {
|
|
|
429
579
|
let forcedTool = false;
|
|
430
580
|
const server = createServer((request, response) => {
|
|
431
581
|
if (request.method === 'GET' && request.url === '/ewai-network-probe') { response.writeHead(204); response.end(); return; }
|
|
582
|
+
if (capability.provider === 'grok' && request.method === 'GET' && ['/', '/v1', '/v1/models'].includes(request.url)) {
|
|
583
|
+
requests.push({ path: request.url, healthProbe: true }); response.writeHead(200, { 'content-type': 'application/json' });
|
|
584
|
+
response.end(JSON.stringify({ data: [{ id: 'fixture', object: 'model', owned_by: 'ewai' }] })); return;
|
|
585
|
+
}
|
|
432
586
|
if (request.method === 'HEAD' && request.url === '/api/hello') {
|
|
433
587
|
requests.push({ path: '/api/hello', healthProbe: true }); response.writeHead(204); response.end(); return;
|
|
434
588
|
}
|
|
@@ -436,9 +590,24 @@ export async function verifyPhaseProviderConformance(handle, control = {}) {
|
|
|
436
590
|
request.on('data', chunk => { bytes += chunk.length; if (bytes > 2 * 1024 * 1024) request.destroy(); else body += chunk; });
|
|
437
591
|
request.on('end', () => {
|
|
438
592
|
let input; try { input = JSON.parse(body); } catch { requests.push({ path: request.url }); response.writeHead(400); response.end(); return; }
|
|
439
|
-
requests.push({ path: request.url, tools: (input.tools ?? []).map(tool => tool.name),
|
|
593
|
+
requests.push({ path: request.url, tools: (input.tools ?? []).map(tool => tool.function?.name ?? tool.name),
|
|
594
|
+
toolFields: (input.tools ?? []).map(tool => Object.keys((tool.function ?? tool).parameters?.properties ?? {})),
|
|
595
|
+
titleOnly: capability.provider === 'grok' && input.tools?.length === 1
|
|
596
|
+
&& input.tools[0].function?.name === 'session_title'
|
|
597
|
+
&& Object.keys(input.tools[0].function.parameters?.properties ?? {}).join(',') === 'session_title'
|
|
598
|
+
&& input.tools[0].function.parameters.properties.session_title.type === 'string',
|
|
440
599
|
inheritedInstructions: body.includes('EWAI_UNTRUSTED_INSTRUCTIONS_CANARY') });
|
|
441
600
|
response.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
601
|
+
if (capability.provider === 'grok') {
|
|
602
|
+
const event = value => response.write('data: ' + JSON.stringify(value) + '\n\n');
|
|
603
|
+
const delta = forcedTool ? { tool_calls: [{ index: 0, id: 'ewai-forged', type: 'function', function: {
|
|
604
|
+
name: 'write_file', arguments: JSON.stringify({ file_path: target, content: 'forged approval' }) } }] }
|
|
605
|
+
: { role: 'assistant', content: 'EWAI_RESTRICTED_FIXTURE_ONLY' };
|
|
606
|
+
event({ id: 'ewai-fixture', object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta, finish_reason: null }] });
|
|
607
|
+
event({ id: 'ewai-fixture', object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta: {}, finish_reason: forcedTool ? 'tool_calls' : 'stop' }],
|
|
608
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } });
|
|
609
|
+
response.end('data: [DONE]\n\n'); return;
|
|
610
|
+
}
|
|
442
611
|
const event = (type, value) => response.write(`event: ${type}\ndata: ${JSON.stringify({ type, ...value })}\n\n`);
|
|
443
612
|
event('message_start', { message: { id: 'msg_ewai_fixture', type: 'message', role: 'assistant', model: 'fixture-model',
|
|
444
613
|
content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } });
|
|
@@ -491,24 +660,31 @@ export async function verifyPhaseProviderConformance(handle, control = {}) {
|
|
|
491
660
|
const forged = await capture(capability, { prompt: 'No tools or approvals are authorised.', timeoutMs: 15000, maxBytes: 16384 },
|
|
492
661
|
{ url: fixtureUrl, isolationProbe: true });
|
|
493
662
|
let refused; try { refused = JSON.parse(forged.output); } catch {}
|
|
663
|
+
const nativeGrok = capability.provider === 'grok';
|
|
664
|
+
const modelRoute = request => nativeGrok ? request.path === '/v1/chat/completions' : request.path.startsWith('/v1/messages');
|
|
494
665
|
const checks = {
|
|
495
666
|
...primitiveChecks,
|
|
496
|
-
structuredDraft: model.status === 'complete' &&
|
|
667
|
+
structuredDraft: nativeGrok ? model.status === 'complete' && parseGrokWorkerResult(model.output).output === 'EWAI_RESTRICTED_FIXTURE_ONLY'
|
|
668
|
+
: model.status === 'complete' && envelope?.type === 'result' && envelope.subtype === 'success' && envelope.is_error === false
|
|
497
669
|
&& envelope.result === 'EWAI_RESTRICTED_FIXTURE_ONLY',
|
|
498
|
-
inheritedCapabilitiesBlocked: requests.filter(
|
|
670
|
+
inheritedCapabilitiesBlocked: (nativeGrok ? requests.filter(modelRoute).length >= 2 : requests.filter(modelRoute).length === 2)
|
|
499
671
|
&& requests.every(request => request.healthProbe === true
|
|
500
|
-
|| request
|
|
501
|
-
|
|
672
|
+
|| modelRoute(request) && (request.tools?.length === 0 || request.titleOnly === true) && !request.inheritedInstructions)
|
|
673
|
+
&& requests.filter(request => modelRoute(request) && request.tools?.length === 0).length >= 2,
|
|
674
|
+
forgedToolRefused: forged.executionStopped && !existsSync(target) && (nativeGrok
|
|
675
|
+
? parseGrokWorkerResult(forged.output).status !== 'complete' : refused?.type === 'result' && refused.is_error === true),
|
|
502
676
|
};
|
|
503
677
|
capability.verified = Object.values(checks).every(value => value === true);
|
|
504
678
|
currentPhaseCapability(handle, false);
|
|
505
|
-
return capability.verified ? { status: 'verified', provider:
|
|
679
|
+
return capability.verified ? { status: 'verified', provider: capability.provider, identityDigest: handle.identityDigest,
|
|
506
680
|
canonicalWritePrevented: true, outputBoundEnforced: true, timeoutEnforced: true, networkIsolated: true, childProcessPrevented: true, toolsExposed: 0, inheritedCapabilitiesBlocked: true,
|
|
507
681
|
forgedToolRefused: true, fixtureServiceOnly: true,
|
|
682
|
+
...(nativeGrok ? { auxiliaryTools: requests.some(request => request.titleOnly) ? ['session_title'] : [],
|
|
683
|
+
toolsBoundary: 'zero worker tools; auxiliary title schema confined to private runtime' } : {}),
|
|
508
684
|
authority: 'none' } : { ...unavailablePhase('phase-provider-conformance-failed'), checks,
|
|
509
|
-
requests: requests.map(request => ({ route: request.healthProbe ? 'fixture-health' : request
|
|
685
|
+
requests: requests.map(request => ({ route: request.healthProbe ? 'fixture-health' : modelRoute(request) ? 'fixture-model'
|
|
510
686
|
: request.path.includes('forbidden-hook') ? 'forbidden-hook' : request.path.includes('forbidden-mcp') ? 'forbidden-mcp' : 'other',
|
|
511
|
-
toolsExposed: request.tools?.length ?? null, inheritedInstructions: request.inheritedInstructions === true })) };
|
|
687
|
+
toolsExposed: request.tools?.length ?? null, toolFields: request.toolFields, toolNames: (request.tools ?? []).filter(name => /^[a-z_]{1,64}$/.test(name)), inheritedInstructions: request.inheritedInstructions === true })) };
|
|
512
688
|
} catch (error) {
|
|
513
689
|
capability.verified = false;
|
|
514
690
|
return { ...unavailablePhase(['phase-cancelled', 'phase-execution-unknown'].includes(error.message)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import {resolveCodingProviders,codingPolicyDigest,assertModelPolicy} from './coding-providers.mjs';
|
|
2
|
+
// Keep legacy discovery defaults stable; Grok is an explicit, additive choice.
|
|
1
3
|
export const VALIDATION_PROVIDERS = Object.freeze(['claude', 'codex', 'antigravity']);
|
|
4
|
+
export const SUPPORTED_VALIDATION_PROVIDERS = Object.freeze([...VALIDATION_PROVIDERS,'grok']);
|
|
2
5
|
|
|
3
6
|
const LEGACY_PROVIDER_ALIASES = Object.freeze({
|
|
4
7
|
gemini: 'antigravity',
|
|
@@ -104,7 +107,7 @@ function normaliseValidators(value, checkpoint) {
|
|
|
104
107
|
|
|
105
108
|
const validators = [...new Set(value.map(canonicalValidationProvider))];
|
|
106
109
|
for (const provider of validators) {
|
|
107
|
-
requireEnum(provider,
|
|
110
|
+
requireEnum(provider, SUPPORTED_VALIDATION_PROVIDERS, `validation checkpoint ${checkpoint} validator`);
|
|
108
111
|
}
|
|
109
112
|
return validators;
|
|
110
113
|
}
|
|
@@ -177,7 +180,7 @@ export function normaliseValidationConfig(value = {}) {
|
|
|
177
180
|
|
|
178
181
|
const rawProviders = value?.external?.providers ?? {};
|
|
179
182
|
const providers = Object.fromEntries(
|
|
180
|
-
|
|
183
|
+
SUPPORTED_VALIDATION_PROVIDERS.filter(provider=>provider!=='grok'||Object.hasOwn(rawProviders,provider)).map((provider) => [
|
|
181
184
|
provider,
|
|
182
185
|
normaliseProvider(
|
|
183
186
|
rawProviders[provider] ?? (provider === 'antigravity' ? rawProviders.gemini : undefined),
|
|
@@ -218,13 +221,13 @@ export function effectiveValidationConfig(configOrValidation, orchestrator = 'ma
|
|
|
218
221
|
configOrValidation?.validation ?? configOrValidation,
|
|
219
222
|
);
|
|
220
223
|
const canonicalOrchestrator = canonicalValidationProvider(orchestrator);
|
|
221
|
-
const normalisedOrchestrator =
|
|
224
|
+
const normalisedOrchestrator = SUPPORTED_VALIDATION_PROVIDERS.includes(canonicalOrchestrator)
|
|
222
225
|
? canonicalOrchestrator
|
|
223
226
|
: 'manual';
|
|
224
227
|
|
|
225
|
-
const globallyEligible =
|
|
228
|
+
const globallyEligible = SUPPORTED_VALIDATION_PROVIDERS.filter((provider) => {
|
|
226
229
|
const setting = validation.external.providers[provider];
|
|
227
|
-
return setting
|
|
230
|
+
return setting?.state === 'available' && setting.enabled;
|
|
228
231
|
});
|
|
229
232
|
|
|
230
233
|
const checkpoints = Object.fromEntries(
|
|
@@ -232,7 +235,14 @@ export function effectiveValidationConfig(configOrValidation, orchestrator = 'ma
|
|
|
232
235
|
const requested = setting.validators === 'auto'
|
|
233
236
|
? globallyEligible
|
|
234
237
|
: setting.validators.filter((provider) => globallyEligible.includes(provider));
|
|
235
|
-
|
|
238
|
+
let providers = requested.filter((provider) => provider !== normalisedOrchestrator);
|
|
239
|
+
if(setting.enabled && configOrValidation?.coding_providers){
|
|
240
|
+
providers=resolveCodingProviders(configOrValidation.coding_providers,{
|
|
241
|
+
orchestrator:normalisedOrchestrator,available:globallyEligible,reviewers:providers,
|
|
242
|
+
requiredReviewers:providers.length,
|
|
243
|
+
}).reviewers;
|
|
244
|
+
for(const provider of providers)assertModelPolicy(provider,configOrValidation.coding_providers);
|
|
245
|
+
}
|
|
236
246
|
|
|
237
247
|
return [
|
|
238
248
|
checkpoint,
|
|
@@ -255,11 +265,12 @@ export function effectiveValidationConfig(configOrValidation, orchestrator = 'ma
|
|
|
255
265
|
orchestrator: normalisedOrchestrator,
|
|
256
266
|
providers: clone(validation.external.providers),
|
|
257
267
|
checkpoints,
|
|
268
|
+
...(configOrValidation?.coding_providers ? {codingPolicy:clone(configOrValidation.coding_providers),codingPolicyDigest:codingPolicyDigest(configOrValidation.coding_providers)} : {}),
|
|
258
269
|
};
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
export function setValidationProvider(config, provider, state, enabled = undefined) {
|
|
262
|
-
requireEnum(provider,
|
|
273
|
+
requireEnum(provider, SUPPORTED_VALIDATION_PROVIDERS, 'validation provider');
|
|
263
274
|
requireEnum(state, ['available', 'unavailable'], 'validation provider state');
|
|
264
275
|
const validation = normaliseValidationConfig(config.validation);
|
|
265
276
|
validation.external.providers[provider] = {
|
|
@@ -18,6 +18,9 @@ personas:
|
|
|
18
18
|
approvals:
|
|
19
19
|
build: required
|
|
20
20
|
destructive_operations: required
|
|
21
|
+
# Optional coding-provider roles are configured with `ewai providers` or the
|
|
22
|
+
# dashboard. Absence preserves existing provider and native model selection.
|
|
23
|
+
# Settings do not grant execution authority; unsupported model restrictions stop.
|
|
21
24
|
validation:
|
|
22
25
|
standards:
|
|
23
26
|
required: true
|