metergraph-cli 0.2.2 → 0.2.4

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.
Files changed (66) hide show
  1. package/README.md +121 -13
  2. package/assets/skill/SKILL.md +124 -31
  3. package/assets/skill/manifest.json +3 -3
  4. package/assets/skills/manifest.json +31 -31
  5. package/assets/skills/metergraph-analyze/SKILL.md +161 -58
  6. package/assets/skills/metergraph-candidates/SKILL.md +50 -8
  7. package/assets/skills/metergraph-evals/SKILL.md +83 -16
  8. package/assets/skills/metergraph-investigate/SKILL.md +90 -34
  9. package/assets/skills/metergraph-iterate/SKILL.md +82 -12
  10. package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
  11. package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
  12. package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
  13. package/assets/skills/metergraph-report/SKILL.md +60 -15
  14. package/assets/skills/metergraph-workloads/SKILL.md +97 -13
  15. package/bin/metergraph.js +1 -1
  16. package/native/README.md +85 -0
  17. package/native/anchored-fs.c +243 -0
  18. package/native/bin/anchored-fs-darwin-arm64 +0 -0
  19. package/native/bin/anchored-fs-darwin-x64 +0 -0
  20. package/native/bin/anchored-fs-linux-arm64 +0 -0
  21. package/native/bin/anchored-fs-linux-x64 +0 -0
  22. package/native/build.mjs +55 -0
  23. package/native/manifest.json +9 -0
  24. package/native/targets.mjs +24 -0
  25. package/package.json +10 -3
  26. package/src/action-help.js +130 -0
  27. package/src/agent-key.js +22 -12
  28. package/src/anchored-fs.js +163 -0
  29. package/src/args.js +335 -31
  30. package/src/auth-binding.js +12 -7
  31. package/src/auth-callback.js +10 -2
  32. package/src/auth-login.js +111 -28
  33. package/src/auth-oauth.js +133 -36
  34. package/src/auth-session.js +19 -12
  35. package/src/auth-store.js +28 -7
  36. package/src/cli.js +147 -15
  37. package/src/constants.js +70 -1
  38. package/src/experiments-contract.js +462 -0
  39. package/src/experiments-output.js +190 -0
  40. package/src/experiments.js +406 -0
  41. package/src/output.js +374 -68
  42. package/src/pricing-args.js +163 -0
  43. package/src/pricing-output.js +206 -0
  44. package/src/pricing-reference.js +195 -0
  45. package/src/pricing.js +307 -0
  46. package/src/read-contract.js +64 -10
  47. package/src/read-output.js +66 -13
  48. package/src/read.js +105 -16
  49. package/src/repository-identity.js +2 -2
  50. package/src/setup-deployment.js +7 -0
  51. package/src/setup-env-git.js +1 -1
  52. package/src/setup-env.js +16 -9
  53. package/src/setup-interactive.js +356 -0
  54. package/src/setup-refusal.js +2 -2
  55. package/src/setup-state.js +9 -4
  56. package/src/setup.js +15 -3
  57. package/src/skill-bundle.js +2 -2
  58. package/src/skill-trusted.js +56 -0
  59. package/src/skill.js +28 -9
  60. package/src/style.js +33 -7
  61. package/src/tools-output.js +73 -0
  62. package/src/tools.js +249 -0
  63. package/src/trace-contract.js +1 -1
  64. package/src/transport.js +26 -3
  65. package/src/verify-output.js +2 -2
  66. package/src/verify.js +5 -2
@@ -0,0 +1,243 @@
1
+ #define _DARWIN_C_SOURCE
2
+ #define _GNU_SOURCE
3
+ #include <sys/types.h>
4
+ #include <sys/stat.h>
5
+ #include <fcntl.h>
6
+ #include <unistd.h>
7
+ #include <errno.h>
8
+ #include <stdint.h>
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+ #include <limits.h>
13
+ #include <signal.h>
14
+
15
+ /* Private, bounded pipe protocol. No paths or file contents are logged. All
16
+ * pathname lookups below the process root are relative to retained dirfds.
17
+ * This process has no network interface or arbitrary execution operation. */
18
+ #define LIMIT (2 * 1024 * 1024)
19
+ #define MAX_HANDLES 8192
20
+ #define STAT_BYTES (11 * 8)
21
+ /* Search permission is sufficient for *at traversal; acquiring a directory
22
+ * capability must not newly require directory-listing permission. */
23
+ #if defined(O_SEARCH)
24
+ #define DIRECTORY_ACCESS O_SEARCH
25
+ #elif defined(O_PATH)
26
+ #define DIRECTORY_ACCESS O_PATH
27
+ #else
28
+ #error "No search-only directory capability API on this target"
29
+ #endif
30
+ struct directory { int fd, parent; char *path, *name; dev_t dev; ino_t ino; };
31
+ static struct directory dirs[MAX_HANDLES];
32
+ static int ndirs = 0, rollback_mode = 0;
33
+ struct entry { int parent; char *name; dev_t dev; ino_t ino; int owned; dev_t own_dev; ino_t own_ino; };
34
+ static struct entry entries[MAX_HANDLES];
35
+ static int nentries;
36
+ static int files[MAX_HANDLES];
37
+ static int nfiles;
38
+ static int readall(int fd, void *data, size_t len) {
39
+ unsigned char *p=data;
40
+ while(len) { ssize_t n=read(fd,p,len); if(n<0 && errno==EINTR) continue; if(n<=0) return -1; p+=n; len-=n; } return 0;
41
+ }
42
+ static int writeall(int fd, const void *data, size_t len) {
43
+ const unsigned char *p=data;
44
+ while(len) { ssize_t n=write(fd,p,len); if(n<0 && errno==EINTR) continue; if(n<=0) return -1; p+=n; len-=n; } return 0;
45
+ }
46
+ static uint32_t get32(const unsigned char *p) { return (uint32_t)p[0]<<24 | (uint32_t)p[1]<<16 | (uint32_t)p[2]<<8 | p[3]; }
47
+ static void put32(unsigned char *p,uint32_t n) { p[0]=n>>24;p[1]=n>>16;p[2]=n>>8;p[3]=n; }
48
+ static void put64(unsigned char *p,uint64_t n) { for(int i=7;i>=0;i--) {p[i]=n&255;n>>=8;} }
49
+ static int reply(int error,const void *data,uint32_t len) {
50
+ unsigned char h[8]; put32(h,error);put32(h+4,len);
51
+ return writeall(1,h,8) || (len && writeall(1,data,len));
52
+ }
53
+ static int basename_ok(const char *p) { return *p && strcmp(p,".") && strcmp(p,"..") && !strchr(p,'/'); }
54
+ static int current(int idx) {
55
+ if(idx==0) return 1;
56
+ struct directory *d=&dirs[idx]; struct stat s;
57
+ if(!current(d->parent)) return 0;
58
+ if(fstatat(dirs[d->parent].fd,d->name,&s,AT_SYMLINK_NOFOLLOW)<0) return 0;
59
+ return S_ISDIR(s.st_mode) && s.st_dev==d->dev && s.st_ino==d->ino;
60
+ }
61
+ static int remember(int parent,const char *name,const struct stat *s);
62
+ static int directory(const char *path) {
63
+ if(!path || path[0]!='/' || strlen(path)>=PATH_MAX) {errno=EINVAL;return -1;}
64
+ for(int i=0;i<ndirs;i++) if(dirs[i].fd>=0 && !strcmp(path,dirs[i].path)) return i;
65
+ if(!strcmp(path,"/")) return 0;
66
+ char *copy=strdup(path); if(!copy) return -1;
67
+ char *slash=strrchr(copy,'/'); char *name=strdup(slash+1);
68
+ if(!name || !basename_ok(name)) {free(copy);free(name);errno=EINVAL;return -1;}
69
+ if(slash==copy) slash[1]=0; else *slash=0;
70
+ int parent=directory(copy); free(copy);
71
+ if(parent<0) {free(name);return -1;}
72
+ int fd=openat(dirs[parent].fd,name,DIRECTORY_ACCESS|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
73
+ if(fd<0) {free(name);return -1;}
74
+ struct stat s;
75
+ if(fstat(fd,&s)<0 || !S_ISDIR(s.st_mode) || ndirs>=MAX_HANDLES) {close(fd);free(name);errno=EIO;return -1;}
76
+ /* A previous directory observation is a capability identity, not permission
77
+ * to adopt whatever object later occupies that spelling. */
78
+ for(int i=0;i<nentries;i++) if(entries[i].parent==parent && !strcmp(entries[i].name,name)) {
79
+ if(entries[i].dev!=s.st_dev || entries[i].ino!=s.st_ino) {close(fd);free(name);errno=ESTALE;return -1;}
80
+ }
81
+ int idx=ndirs++;
82
+ dirs[idx]=(struct directory){fd,parent,strdup(path),name,s.st_dev,s.st_ino};
83
+ remember(parent,name,&s);
84
+ return idx;
85
+ }
86
+ static int parent_for(const char *path,char **name) {
87
+ if(!path || path[0]!='/' || strlen(path)>=PATH_MAX) {errno=EINVAL;return -1;}
88
+ char *copy=strdup(path); if(!copy) return -1;
89
+ char *slash=strrchr(copy,'/'); *name=strdup(slash+1);
90
+ if(!*name || !basename_ok(*name)) {free(copy);free(*name);*name=NULL;errno=EINVAL;return -1;}
91
+ if(slash==copy) slash[1]=0; else *slash=0;
92
+ int parent=directory(copy);free(copy);return parent;
93
+ }
94
+ static int mutable_parent(int idx) { if(idx<0)return 0; if(!rollback_mode && !current(idx)) {errno=ESTALE;return 0;} return 1; }
95
+ static int remember(int parent,const char *name,const struct stat *s) {
96
+ for(int i=0;i<nentries;i++) if(entries[i].parent==parent && !strcmp(entries[i].name,name)) {
97
+ entries[i].dev=s->st_dev;entries[i].ino=s->st_ino;return 0;
98
+ }
99
+ if(nentries>=MAX_HANDLES) {errno=EMFILE;return -1;}
100
+ entries[nentries++]=(struct entry){parent,strdup(name),s->st_dev,s->st_ino,0,0,0};return 0;
101
+ }
102
+ static int own(int parent,const char *name,const struct stat *s) {
103
+ if(remember(parent,name,s)<0)return -1;
104
+ for(int i=0;i<nentries;i++) if(entries[i].parent==parent && !strcmp(entries[i].name,name)) {entries[i].owned=1;entries[i].own_dev=s->st_dev;entries[i].own_ino=s->st_ino;return 0;}
105
+ errno=EIO;return -1;
106
+ }
107
+ static int promote(int parent,const char *name,const struct stat *s) {
108
+ if(remember(parent,name,s)<0)return -1;
109
+ for(int i=0;i<nentries;i++) if(entries[i].parent==parent && !strcmp(entries[i].name,name)) {entries[i].owned=0;return 0;}
110
+ errno=EIO;return -1;
111
+ }
112
+ static int expected(int parent,const char *name,int use_owned) {
113
+ for(int i=0;i<nentries;i++) if(entries[i].parent==parent && !strcmp(entries[i].name,name)) {
114
+ struct stat s; if(fstatat(dirs[parent].fd,name,&s,AT_SYMLINK_NOFOLLOW)<0)return -1;
115
+ dev_t dev=use_owned && entries[i].owned?entries[i].own_dev:entries[i].dev;
116
+ ino_t ino=use_owned && entries[i].owned?entries[i].own_ino:entries[i].ino;
117
+ if(s.st_dev!=dev || s.st_ino!=ino) {errno=ESTALE;return -1;}
118
+ return 0;
119
+ }
120
+ errno=ESTALE;return -1;
121
+ }
122
+ static void statbytes(unsigned char *out,const struct stat *s) {
123
+ #if defined(__APPLE__)
124
+ struct timespec modified=s->st_mtimespec, changed=s->st_ctimespec;
125
+ #else
126
+ struct timespec modified=s->st_mtim, changed=s->st_ctim;
127
+ #endif
128
+ /* Keep seconds and nanoseconds separate. Multiplying time_t by 1e9 here
129
+ * would overflow for timestamps which JavaScript BigInt can represent. */
130
+ uint64_t fields[]={s->st_dev,s->st_ino,s->st_mode,s->st_nlink,s->st_uid,s->st_gid,s->st_size,
131
+ (uint64_t)modified.tv_sec,(uint64_t)modified.tv_nsec,(uint64_t)changed.tv_sec,(uint64_t)changed.tv_nsec};
132
+ for(int i=0;i<11;i++)put64(out+i*8,fields[i]);
133
+ }
134
+ static int filefd(const char *s) { char *end;long n=strtol(s,&end,10); if(*end || n<0 || n>=nfiles || files[n]<0) {errno=EBADF;return -1;}return files[n]; }
135
+ int main(void) {
136
+ /* The JS owner defers termination until transaction cleanup completes.
137
+ * Group-delivered terminal signals must not revoke its live capabilities.
138
+ * Parent death closes stdin, so the loop exits without a signal handler. */
139
+ signal(SIGINT,SIG_IGN);signal(SIGTERM,SIG_IGN);signal(SIGHUP,SIG_IGN);
140
+ int root=open("/",DIRECTORY_ACCESS|O_DIRECTORY|O_CLOEXEC);struct stat rs;
141
+ if(root<0 || fstat(root,&rs)<0)return 1;
142
+ dirs[ndirs++]=(struct directory){root,-1,strdup("/"),strdup(""),rs.st_dev,rs.st_ino};
143
+ for(;;) {
144
+ unsigned char header[4]; if(readall(0,header,4))break;
145
+ uint32_t length=get32(header);if(length<2 || length>LIMIT) return 2;
146
+ unsigned char *request=malloc(length+1);if(!request)return 3;
147
+ if(readall(0,request,length)) {free(request);break;}request[length]=0;
148
+ int op=request[0],argc=request[1];char *args[8]={0};size_t pos=2;int malformed=argc>8;
149
+ for(int i=0;i<argc && !malformed;i++) {
150
+ if(pos+4>length) {malformed=1;break;}
151
+ uint32_t len=get32(request+pos);pos+=4;
152
+ if(pos+len>length || memchr(request+pos,0,len)) {malformed=1;break;}
153
+ args[i]=malloc(len+1);if(!args[i]) {malformed=1;break;}
154
+ memcpy(args[i],request+pos,len);args[i][len]=0;pos+=len;
155
+ }
156
+ int error=0; unsigned char out[STAT_BYTES];void *output=out;uint32_t outlen=0;
157
+ int parent=-1,fd=-1,result=-1;char *name=NULL;struct stat st;
158
+ errno=0;
159
+ if(malformed) error=EINVAL;
160
+ else switch(op) {
161
+ case 1: /* lstat: never follows the final entry */
162
+ if(argc!=1) {error=EINVAL;break;}
163
+ if(!strcmp(args[0],"/")) result=fstat(root,&st);
164
+ else {parent=parent_for(args[0],&name);if(parent<0)break;result=fstatat(dirs[parent].fd,name,&st,AT_SYMLINK_NOFOLLOW);}
165
+ if(!result) {
166
+ if(S_ISDIR(st.st_mode)) {
167
+ int pinned=directory(args[0]);struct stat opened;
168
+ if(pinned<0) {result=-1;break;}
169
+ if(fstat(dirs[pinned].fd,&opened)<0 || opened.st_dev!=st.st_dev || opened.st_ino!=st.st_ino) {errno=ESTALE;result=-1;break;}
170
+ }
171
+ statbytes(out,&st);outlen=STAT_BYTES;if(parent>=0 && !S_ISLNK(st.st_mode))remember(parent,name,&st);
172
+ }
173
+ break;
174
+ case 2: /* open: protocol flags are independent of host constants */
175
+ if(argc!=3) {error=EINVAL;break;}
176
+ parent=parent_for(args[0],&name);if(parent<0)break;
177
+ int kind=atoi(args[1]),flags=O_NOFOLLOW|O_CLOEXEC|O_NONBLOCK;
178
+ if(kind==0)flags|=O_RDONLY;else if(kind==1)flags|=O_RDWR;else if(kind==2)flags|=O_WRONLY|O_CREAT|O_EXCL;else {error=EINVAL;break;}
179
+ if(kind!=0 && !mutable_parent(parent))break;
180
+ fd=openat(dirs[parent].fd,name,flags,strtoul(args[2],NULL,10)&0777);
181
+ if(fd<0)break;
182
+ if(fstat(fd,&st)<0 || (!S_ISREG(st.st_mode) && !(kind==0 && S_ISDIR(st.st_mode))) || (S_ISREG(st.st_mode) && st.st_nlink!=1)) {close(fd);fd=-1;errno=ELOOP;break;}
183
+ if(nfiles>=MAX_HANDLES) {close(fd);errno=EMFILE;break;}
184
+ if((kind==2?own(parent,name,&st):remember(parent,name,&st))<0) {close(fd);break;}
185
+ put32(out,nfiles);outlen=4;files[nfiles++]=fd;result=0;break;
186
+ case 3: /* fstat */
187
+ if(argc!=1) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;result=fstat(fd,&st);if(!result){statbytes(out,&st);outlen=STAT_BYTES;}break;
188
+ case 4: /* bounded read from descriptor */
189
+ if(argc!=1) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;
190
+ if(fstat(fd,&st)<0)break;
191
+ if(!S_ISREG(st.st_mode) || st.st_size<0 || st.st_size>LIMIT) {errno=EFBIG;break;}
192
+ output=malloc(LIMIT+1);if(!output){errno=ENOMEM;break;}
193
+ size_t used=0;ssize_t got;
194
+ while(used<LIMIT && (got=read(fd,(char*)output+used,LIMIT-used))>0)used+=got;
195
+ if(got<0){break;}outlen=used;result=0;break;
196
+ case 5: /* write payload */
197
+ if(argc!=1) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;result=writeall(fd,request+pos,length-pos);break;
198
+ case 6:
199
+ if(argc!=2) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;result=fchmod(fd,strtoul(args[1],NULL,10)&0777);break;
200
+ case 7:
201
+ if(argc!=1) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;result=fsync(fd);break;
202
+ case 8:
203
+ if(argc!=1) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;result=close(fd);files[atoi(args[0])]=-1;break;
204
+ case 9: /* mkdir one basename */
205
+ if(argc!=2) {error=EINVAL;break;}parent=parent_for(args[0],&name);if(!mutable_parent(parent))break;
206
+ result=mkdirat(dirs[parent].fd,name,strtoul(args[1],NULL,10)&0777);
207
+ if(!result && !fstatat(dirs[parent].fd,name,&st,AT_SYMLINK_NOFOLLOW)) {own(parent,name,&st);}
208
+ break;
209
+ case 10: /* rename, anchored at both parents */
210
+ if(argc!=2) {error=EINVAL;break;}parent=parent_for(args[0],&name);if(!mutable_parent(parent) || expected(parent,name,1)<0)break;
211
+ char *destname=NULL;int dest=parent_for(args[1],&destname);
212
+ if(mutable_parent(dest)) {
213
+ if(!fstatat(dirs[dest].fd,destname,&st,AT_SYMLINK_NOFOLLOW) && (S_ISLNK(st.st_mode) || !S_ISREG(st.st_mode) || st.st_nlink!=1 || expected(dest,destname,0)<0)) {errno=ESTALE;}
214
+ else { result=renameat(dirs[parent].fd,name,dirs[dest].fd,destname);if(!result && !fstatat(dirs[dest].fd,destname,&st,AT_SYMLINK_NOFOLLOW))promote(dest,destname,&st); }
215
+ }free(destname);break;
216
+ case 11: case 12: /* unlink/rmdir only an object previously observed */
217
+ if(argc!=1) {error=EINVAL;break;}parent=parent_for(args[0],&name);if(!mutable_parent(parent) || expected(parent,name,1)<0)break;
218
+ result=unlinkat(dirs[parent].fd,name,op==12?AT_REMOVEDIR:0);
219
+ if(!result && op==12) for(int i=1;i<ndirs;i++) if(dirs[i].parent==parent && !strcmp(dirs[i].name,name) && dirs[i].fd>=0) {close(dirs[i].fd);dirs[i].fd=-1;}
220
+ break;
221
+ case 13: /* validate existing directory ancestry */
222
+ if(argc!=1) {error=EINVAL;break;}parent=directory(args[0]);if(parent<0)break;
223
+ if(current(parent))result=0;else errno=ESTALE;break;
224
+ case 15: /* exclusive hardlink promotion, never follows source */
225
+ if(argc!=2) {error=EINVAL;break;}parent=parent_for(args[0],&name);if(!mutable_parent(parent) || expected(parent,name,1)<0)break;
226
+ char *linkname=NULL;int linkparent=parent_for(args[1],&linkname);
227
+ if(mutable_parent(linkparent)) {result=linkat(dirs[parent].fd,name,dirs[linkparent].fd,linkname,0);if(!result && !fstatat(dirs[linkparent].fd,linkname,&st,AT_SYMLINK_NOFOLLOW))promote(linkparent,linkname,&st);}free(linkname);break;
228
+ case 16: /* positional bounded read */
229
+ if(argc!=3) {error=EINVAL;break;}fd=filefd(args[0]);if(fd<0)break;
230
+ size_t amount=strtoul(args[1],NULL,10);if(amount>LIMIT){error=EFBIG;break;}
231
+ output=malloc(amount?amount:1);if(!output){error=ENOMEM;break;}
232
+ ssize_t count=pread(fd,output,amount,strtoll(args[2],NULL,10));if(count>=0){outlen=count;result=0;}break;
233
+ case 14:
234
+ if(argc!=1 || (strcmp(args[0],"0") && strcmp(args[0],"1"))) {error=EINVAL;break;}rollback_mode=atoi(args[0]);result=0;break;
235
+ default:error=EINVAL;
236
+ }
237
+ if(!error && result<0)error=errno?errno:EIO;
238
+ if(reply(error,error?NULL:output,error?0:outlen))return 4;
239
+ if(output!=out)free(output);
240
+ free(name);for(int i=0;i<argc && i<8;i++)free(args[i]);free(request);
241
+ }
242
+ return 0;
243
+ }
Binary file
@@ -0,0 +1,55 @@
1
+ // Development/release build only. Package consumers never run a compiler.
2
+ import { spawnSync } from "node:child_process";
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { TARGETS, DIGEST, REVISION, binaryFor, exactKeys, argumentsFor } from "./targets.mjs";
8
+ const root = path.dirname(fileURLToPath(import.meta.url));
9
+ const sha256 = bytes => createHash("sha256").update(bytes).digest("hex");
10
+ function directory(dir) {
11
+ fs.mkdirSync(dir, {recursive:true});
12
+ if (!fs.lstatSync(dir).isDirectory() || fs.lstatSync(dir).isSymbolicLink()) throw new Error("unsafe build output");
13
+ }
14
+ export function build({output, revision, target = `${process.platform}-${process.arch}`} = {}) {
15
+ if (!TARGETS.includes(target) || target !== `${process.platform}-${process.arch}`) throw new Error("unsupported build host");
16
+ if (revision === undefined) {
17
+ revision = process.env.GITHUB_SHA;
18
+ if (revision === undefined) {
19
+ const git = spawnSync("git", ["rev-parse", "HEAD"], {cwd:root, encoding:"utf8"});
20
+ if (git.status !== 0) throw new Error("source revision unavailable");
21
+ revision = git.stdout.trim();
22
+ }
23
+ }
24
+ if (!REVISION.test(revision)) throw new Error("invalid source revision");
25
+ const provenanceOutput = output !== undefined;
26
+ output = path.resolve(output ?? root); directory(output); directory(path.join(output,"bin"));
27
+ const manifestPath = path.join(output,"manifest.json");
28
+ let artifacts = {};
29
+ if (fs.existsSync(manifestPath)) {
30
+ if (!fs.lstatSync(manifestPath).isFile() || fs.lstatSync(manifestPath).isSymbolicLink()) throw new Error("unsafe build manifest");
31
+ const previous = JSON.parse(fs.readFileSync(manifestPath,"utf8"));
32
+ if (!exactKeys(previous,["schema","artifacts"]) || previous.schema !== 1 || !exactKeys(previous.artifacts,Object.keys(previous.artifacts ?? {}))) throw new Error("invalid build manifest");
33
+ for (const [name,digest] of Object.entries(previous.artifacts)) {
34
+ if (!TARGETS.some(value=>binaryFor(value)===name) || !DIGEST.test(digest)) throw new Error("unknown build artifact");
35
+ const file=path.join(output,"bin",name);const stat=fs.lstatSync(file);
36
+ if (!stat.isFile() || stat.isSymbolicLink() || sha256(fs.readFileSync(file)) !== digest) throw new Error("existing artifact integrity failed");
37
+ artifacts[name]=digest;
38
+ }
39
+ }
40
+ const name = binaryFor(target); const temporary = path.join(output,"bin",`.build-${randomBytes(12).toString("hex")}`);
41
+ try {
42
+ const result = spawnSync("clang", ["-std=c11", "-Wall", "-Wextra", "-Werror", "-O2", path.join(root,"anchored-fs.c"), "-o",temporary], {stdio:"inherit"});
43
+ if (result.status !== 0) throw new Error("native compilation failed");
44
+ fs.chmodSync(temporary,0o755);
45
+ const digest = sha256(fs.readFileSync(temporary)); fs.renameSync(temporary,path.join(output,"bin",name)); artifacts[name]=digest;
46
+ const ordered = Object.fromEntries(Object.entries(artifacts).sort(([a],[b])=>a.localeCompare(b)));
47
+ fs.writeFileSync(manifestPath,`${JSON.stringify({schema:1,artifacts:ordered},null,2)}\n`);
48
+ if (provenanceOutput) fs.writeFileSync(path.join(output,"provenance.json"),`${JSON.stringify({schema:1,source_revision:revision,source_sha256:sha256(fs.readFileSync(path.join(root,"anchored-fs.c"))),target,binary_sha256:digest},null,2)}\n`);
49
+ return {target,digest};
50
+ } finally {try {fs.unlinkSync(temporary);} catch(error) {if(error.code!=="ENOENT")throw error;} }
51
+ }
52
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
53
+ try {const args=argumentsFor(process.argv.slice(2),[],["--output","--revision"]);build({output:args["--output"],revision:args["--revision"]});}
54
+ catch(error) {process.stderr.write(`${error.message}\n`);process.exitCode=1;}
55
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema": 1,
3
+ "artifacts": {
4
+ "anchored-fs-darwin-arm64": "787a6d122add60a261fc6c70717f270825af3be7512d7dea4104f608293370b6",
5
+ "anchored-fs-darwin-x64": "f569e48393b0650a93c8aaf2ef8984fd0dbf90e33f51d847db3c629a7c8fbd38",
6
+ "anchored-fs-linux-arm64": "6ac3eca79a8211a32dfd76eebeffe0b4b4e6d1540bacad739856b9c91094b308",
7
+ "anchored-fs-linux-x64": "401e2fd89ca15cd9d68c4e5077ca08a9caf0fe93434a0b243a361845b1284533"
8
+ }
9
+ }
@@ -0,0 +1,24 @@
1
+ // Build and release allowlist. Presence here is not evidence of verification;
2
+ // CI must execute the compiled helper on each declared host before assembly.
3
+ export const TARGETS = Object.freeze(["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]);
4
+ export const BINARY_PREFIX = "anchored-fs-";
5
+ export const REVISION = /^[0-9a-f]{40}$/;
6
+ export const DIGEST = /^[0-9a-f]{64}$/;
7
+ export function binaryFor(target) {
8
+ if (!TARGETS.includes(target)) throw new Error("unsupported native target");
9
+ return `${BINARY_PREFIX}${target}`;
10
+ }
11
+ export function exactKeys(value, keys) {
12
+ return value !== null && typeof value === "object" && !Array.isArray(value) &&
13
+ Object.keys(value).sort().join() === [...keys].sort().join();
14
+ }
15
+ export function argumentsFor(argv, required, optional = []) {
16
+ const allowed = new Set([...required, ...optional]); const result = {};
17
+ for (let i = 0; i < argv.length; i += 2) {
18
+ const name = argv[i]; const value = argv[i + 1];
19
+ if (!allowed.has(name) || Object.hasOwn(result, name) || typeof value !== "string" || value === "" || value.startsWith("--")) throw new Error("invalid build arguments");
20
+ result[name] = value;
21
+ }
22
+ if (required.some(name => !Object.hasOwn(result, name))) throw new Error("missing build arguments");
23
+ return result;
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metergraph-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Metergraph command line tool with a read-only connection probe, a project skill installer, Metadata-only sign in and bounded workspace Metadata reads",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,12 @@
12
12
  "files": [
13
13
  "bin/metergraph.js",
14
14
  "src/*.js",
15
+ "native/anchored-fs.c",
16
+ "native/build.mjs",
17
+ "native/targets.mjs",
18
+ "native/manifest.json",
19
+ "native/README.md",
20
+ "native/bin/*",
15
21
  "assets/skill/SKILL.md",
16
22
  "assets/skill/manifest.json",
17
23
  "assets/skills/manifest.json",
@@ -20,8 +26,9 @@
20
26
  "README.md"
21
27
  ],
22
28
  "scripts": {
23
- "test": "node --test test/*.test.js",
24
- "test:package": "node --test test/package/*.test.js"
29
+ "build:native": "node native/build.mjs",
30
+ "test": "node test/run-tests.js",
31
+ "test:package": "node test/run-tests.js package"
25
32
  },
26
33
  "engines": {
27
34
  "node": ">=22"
@@ -0,0 +1,130 @@
1
+ // Human action help; command execution and JSON envelopes remain independent.
2
+ const option = (name, value, summary) => ({ name, value, summary });
3
+ const selectors = [
4
+ option("--model", "ID", "Model ID."),
5
+ option("--provider", "ID", "Provider; use instead of --channel."),
6
+ option("--channel", "ID", "Billing channel; use instead of --provider."),
7
+ option("--region", "ID", "Billing region."),
8
+ ];
9
+ const pricingCommon = [
10
+ option("--url", "ORIGIN", "Service origin. Default: https://app.metergraph.dev."),
11
+ option("--timeout-ms", "N", "Request deadline, 100–30000 ms. Default: 10000."),
12
+ option("--api", "API", "public (default) or reference. Reference requires a loopback origin."),
13
+ option("--json", null, "Print JSON."),
14
+ ];
15
+ const tokens = ["input", "output", "cache-read", "cache-write", "cache-write-5m", "cache-write-1h"].map(
16
+ name => option(`--${name}-tokens`, "N", name.startsWith("cache-write")
17
+ ? "Anthropic cache-write tokens."
18
+ : "Token count. Omitted counts stay unknown."));
19
+ const pricing = {
20
+ models: ["Discover available models", "[--provider ID | --channel ID] [--limit N]", "metergraph pricing models --limit 10",
21
+ [...selectors, option("--limit", "N", "Models to show. Default: 20; max: 100.")],
22
+ "Current catalog. No account required."],
23
+ lookup: ["Look up a model's prices", "--model ID [--provider ID | --channel ID]", "metergraph pricing lookup --model gpt-5 --provider openai",
24
+ [...selectors, option("--at", "YYYY-MM-DD", "Price date (UTC). Default: now.")],
25
+ "Public catalog prices. No account required."],
26
+ calculate: ["Estimate cost from token counts", "--model ID [--input-tokens N] [--output-tokens N]", "metergraph pricing calculate --model gpt-5 --provider openai --input-tokens 1000 --output-tokens 200",
27
+ [...selectors, ...tokens, option("--batch", null, "Use batch rates."),
28
+ option("--at", "YYYY-MM-DD", "Price date (UTC). Default: now."),
29
+ option("--trace-format", "FORMAT", "Token format: anthropic.messages, openai.responses, openai.chat_completions or otel.gen_ai.")],
30
+ "Only supplied counts are sent. Partial costs are known subtotals. OpenAI cache reads cannot exceed total input. No model calls are made."],
31
+ market: ["Compare current rates on one billing channel", "--channel ID [--region ID] [--limit N]", "metergraph pricing market --channel openai-api --limit 10",
32
+ selectors.filter(o => ["--channel", "--region"].includes(o.name)).concat(option("--limit", "N", "Models to show. Default: 20; max: 100.")),
33
+ "Current prices, without historical or quality rankings. No account required."],
34
+ history: ["Inspect historical catalog prices", "--model ID --channel ID --api reference --url ORIGIN", "metergraph pricing history --model openai/gpt-5.6-terra --channel openai-api --api reference --url http://127.0.0.1:8765 --format chart",
35
+ selectors.filter(o => o.name !== "--provider").concat([
36
+ option("--api", "reference", "Required. History currently uses the local reference adapter."),
37
+ option("--at", "YYYY-MM-DD", "Reference lookup date at UTC midnight."),
38
+ option("--format", "FORMAT", "chart (default), table or html. Cannot combine with --json."),
39
+ ]), "Requires a loopback reference service. Historical data is not available from the public API."],
40
+ };
41
+
42
+ export function actionHelpData(topic, action, commands) {
43
+ if (topic === "pricing") {
44
+ const canonical = action === "cost" ? "calculate" : action;
45
+ const entry = pricing[canonical];
46
+ if (!entry) return null;
47
+ const [summary, usage, example, options, note] = entry;
48
+ const focused = options.map(o => {
49
+ if (o.name === "--model") return { ...o, summary: canonical === "models" ? "Filter by model ID." : "Required. Model ID." };
50
+ if (o.name === "--channel" && ["market", "history"].includes(canonical)) return { ...o, summary: "Required. Billing channel." };
51
+ if (o.name === "--region" && ["market", "history"].includes(canonical)) return { ...o, summary: "Billing region. Default: global." };
52
+ return o;
53
+ });
54
+ return { summary, usage: `metergraph pricing ${action} ${usage} [options]`,
55
+ options: [...focused, ...pricingCommon.filter(o => !options.some(specific => specific.name === o.name)
56
+ && !(o.name === "--api" && ["models", "calculate"].includes(action)))], examples: [example.replace("pricing calculate", `pricing ${action}`)], note };
57
+ }
58
+ if (topic === "experiments") {
59
+ const entry = commands.find(c => c.name === topic);
60
+ const names = { list: ["--workload", "--limit"], get: ["--id"],
61
+ estimate: ["--workload", "--treatment", "--last-n", "--sample-model", "--track"],
62
+ start: ["--workload", "--treatment", "--last-n", "--sample-model", "--track"] }[action];
63
+ if (!names) return null;
64
+ const specific = ["--workload", "--limit", "--id", "--treatment", "--last-n", "--sample-model", "--track"];
65
+ const options = entry.options.filter(o => !specific.includes(o.name) || names.includes(o.name)).map(o => {
66
+ if (o.name === "--agent-token-file") return { ...o, summary: action === "start"
67
+ ? "Private (0600) Debug key file with Allow analysis starts. Or set METERGRAPH_AGENT_TOKEN."
68
+ : "Private (0600) Metadata key file. Or use saved sign in or METERGRAPH_AGENT_TOKEN." };
69
+ const summaries = {
70
+ "--workload": action === "list" ? "Filter by workload ID." : "Required. Workload ID.",
71
+ "--id": "Required. Experiment ID.", "--limit": "Experiments to show. Default: 20; max: 50.",
72
+ "--treatment": "Required. PROVIDER/MODEL; repeat for up to 3 treatments.",
73
+ "--last-n": "Calls to replay. Default: 20; range: 10–40.",
74
+ "--sample-model": "Filter production calls by model.", "--track": "JSON path to compare; repeat up to 10 times.",
75
+ };
76
+ return { ...o, summary: summaries[o.name] ?? o.summary };
77
+ });
78
+ const invocation = { list: "[--workload ID] [--limit N]", get: "--id UUID",
79
+ estimate: "--workload ID --treatment PROVIDER/MODEL", start: "--workload ID --treatment PROVIDER/MODEL" }[action];
80
+ return { summary: { list: "List recent experiments", get: "Read an experiment's results",
81
+ estimate: "Estimate a model comparison before starting it", start: "Start a budgeted model comparison" }[action],
82
+ usage: `metergraph experiments ${action} ${invocation} [options]`, options,
83
+ examples: [`metergraph experiments ${action} ${{ list: "--limit 10", get: "--id UUID", estimate: "--workload ROUTE --treatment PROVIDER/MODEL", start: "--workload ROUTE --treatment PROVIDER/MODEL --agent-token-file /path/to/private-key" }[action]}`],
84
+ note: action === "start"
85
+ ? "Replays captured production prompts on the selected providers and incurs provider costs. Requires a Debug key with Allow analysis starts. Estimate first. Each start creates a new experiment; never retry a successful start."
86
+ : { list: "Uses Metadata access. Returns experiment summaries.",
87
+ get: "Uses Metadata access. Returns results without prompts or responses.",
88
+ estimate: "Uses Metadata access. Estimates provider cost without starting a replay." }[action] };
89
+ }
90
+ const entry = commands.find(c => c.name === `${topic} ${action}`);
91
+ if (!entry) return null;
92
+ const label = topic === "skill" ? "setup skill" : "workflow skills";
93
+ return { summary: action === "list" ? "List installed workflow skills"
94
+ : `${action === "install" ? "Install" : "Update"} the Metergraph ${label}`,
95
+ usage: `metergraph ${topic} ${action}${action === "list" ? "" : " --client codex|claude|cursor"} [options]`,
96
+ options: entry.options, examples: [`metergraph ${topic} ${action}${action === "list" ? "" : " --client codex"}`],
97
+ note: action === "list" ? "Lists installed workflow skills for all clients. Makes no changes."
98
+ : (action === "install" ? "Existing skills are left alone. No network requests." : "Replaces only unchanged skills owned by this CLI. No network requests.") };
99
+ }
100
+
101
+ // Connection details remain available offline without dominating everyday help.
102
+ const ADVANCED_CONNECTION = new Set(["--url", "--config-dir", "--workspace", "--agent-token-file", "--timeout-ms", "--api"]);
103
+ const SHORT_OPTIONS = {
104
+ "--project": "Project directory. Default: current directory.",
105
+ "--config-dir": "Credential directory. Default: per-user config directory.",
106
+ "--url": "Service origin. Default: project binding or hosted service.",
107
+ "--workspace": "Require this workspace ID.",
108
+ "--json": "Print JSON.",
109
+ "--client": "Agent: codex, claude or cursor.",
110
+ "--runtime": "local, cloud or cloud-no-shell. Inferred in a local terminal.",
111
+ "--skip-skill": "Skip agent skill installation.",
112
+ "--env-file": "Private env file. Default: .env.",
113
+ "--no-browser": "Print the approval link instead of opening it.",
114
+ "--signup": "Open sign up during browser approval.",
115
+ "--reconnect": "Allow switching the connected workspace.",
116
+ "--repair": "Approve replacement of a key that no longer verifies.",
117
+ "--deployment": "managed (default), customer-local, byoc or oss.",
118
+ "--confirm-prerequisites": "Confirm operator prerequisites are met.",
119
+ "--repository": "Repository identity as OWNER/NAME.",
120
+ "--no-repository": "Leave repository identity unset.",
121
+ };
122
+ export function displayOptions(options, topic, action, advanced = false) {
123
+ return options.filter(o => advanced || !ADVANCED_CONNECTION.has(o.name)
124
+ // History cannot run without these explicit connection selectors.
125
+ || (topic === "pricing" && action === "history" && ["--api", "--url"].includes(o.name)))
126
+ .map(o => ({ ...o, summary: advanced ? o.summary
127
+ : o.name === "--runtime" && ["skill", "skills"].includes(topic) ? "local (default) or cloud."
128
+ : o.name === "--url" && topic === "pricing" ? o.summary
129
+ : SHORT_OPTIONS[o.name] ?? o.summary }));
130
+ }
package/src/agent-key.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { verifyContext } from "./auth-oauth.js";
2
- import { DEFAULT_ORIGIN, MAX_BODY_BYTES, METADATA_SCOPE } from "./constants.js";
2
+ import { DEFAULT_ORIGIN, MAX_BODY_BYTES } from "./constants.js";
3
3
  import { CREDENTIAL_MIN_LENGTH, readCredentialFile } from "./deployment-credential.js";
4
4
  import { checkDeployment } from "./doctor.js";
5
5
  import { AGENT_TOKEN_ENV, AGENT_URL_ENV } from "./environment.js";
6
6
  import { parseOrigin } from "./origin.js";
7
7
  import { send } from "./transport.js";
8
8
 
9
- // The read commands' second credential: a coding-agent key with Metadata
10
- // access that a person created on the Keys page and handed to the agent's
11
- // environment as a secret. It is for agents where browser sign in cannot
12
- // finish (SSH, cloud workspaces, CI, some containers).
9
+ // The read commands' second credential: a coding-agent key that a person
10
+ // created on the Keys page (or with a bundle's bin/agent-key) and handed to
11
+ // the agent's environment as a secret. It has the Metadata role, or the
12
+ // Debug role with workflow permissions. It is how agents read where browser
13
+ // sign in cannot finish (SSH, cloud workspaces, CI, some containers), and how
14
+ // they get the Debug role and permissions.
13
15
  //
14
16
  // The key comes only from the METERGRAPH_AGENT_TOKEN environment variable or
15
17
  // from a private file named by --agent-token-file (checked as setup checks
@@ -17,10 +19,11 @@ import { send } from "./transport.js";
17
19
  // written anywhere, never printed, and sent only as a bearer header to the
18
20
  // chosen origin, and only after that origin has answered the unauthenticated
19
21
  // deployment probe as a supported Metergraph service. Before any read, the
20
- // service's own workspace and capabilities documents must show exactly the
21
- // Metadata scope, no content access, the expected deployment profile and,
22
- // when one is pinned, the expected workspace. A broader key fails closed:
23
- // there is no fallback to Debug or Replay access.
22
+ // service's own workspace and capabilities documents must show Metadata, or
23
+ // Debug with workflow permissions, the expected deployment profile and, when
24
+ // one is pinned, the expected workspace. A Replay key, or one with any other
25
+ // scope, fails closed. The read commands send only Metadata requests,
26
+ // whichever key they use.
24
27
 
25
28
  const TOKEN = new RegExp(`^[\\x21-\\x7e]{${CREDENTIAL_MIN_LENGTH},8192}$`);
26
29
 
@@ -66,6 +69,8 @@ export function keyTarget({ binding, options, env, originConfirmed = false }) {
66
69
  ok: true,
67
70
  origin: binding.origin,
68
71
  workspaceId: binding.workspace_id,
72
+ // A slug or name to match against the verified workspace.
73
+ workspaceText: options.workspaceText ?? null,
69
74
  profile: binding.deployment_profile,
70
75
  originSource: "binding",
71
76
  };
@@ -74,6 +79,7 @@ export function keyTarget({ binding, options, env, originConfirmed = false }) {
74
79
  ok: true,
75
80
  origin: requested ?? DEFAULT_ORIGIN,
76
81
  workspaceId: pinned,
82
+ workspaceText: options.workspaceText ?? null,
77
83
  profile: null,
78
84
  originSource: source ?? "default",
79
85
  };
@@ -84,7 +90,10 @@ export function keyTarget({ binding, options, env, originConfirmed = false }) {
84
90
  // { ok: false, outcome, reason }
85
91
  // verifiedProfile is the profile the caller already read from GET
86
92
  // /v1/deployment under the same deadline, or null to read it here.
87
- export async function keySession({ target, source, env, signal, verifiedProfile = null }) {
93
+ // access is "read" for the read commands, or "experiment_start" for
94
+ // experiments start, which needs Debug access and the start grant
95
+ // (verifyContext).
96
+ export async function keySession({ target, source, env, signal, verifiedProfile = null, access = "read" }) {
88
97
  // A key already in the environment is checked for shape before any
89
98
  // request; a key file is opened only once the origin has proven itself.
90
99
  let key = source.kind === "env" ? readAgentKey(source, env) : null;
@@ -108,7 +117,7 @@ export async function keySession({ target, source, env, signal, verifiedProfile
108
117
  const verified = await verifyContext(
109
118
  target.origin,
110
119
  token,
111
- { profile, workspaceId: target.workspaceId, anyWorkspace: target.workspaceId === null },
120
+ { profile, workspaceId: target.workspaceId, anyWorkspace: target.workspaceId === null, access },
112
121
  signal,
113
122
  );
114
123
  if (!verified.ok) {
@@ -125,7 +134,8 @@ export async function keySession({ target, source, env, signal, verifiedProfile
125
134
  origin: target.origin,
126
135
  workspaceId: verified.workspaceId,
127
136
  profile,
128
- scopes: [METADATA_SCOPE],
137
+ access: verified.access,
138
+ scopes: verified.scopes,
129
139
  accessToken: token,
130
140
  },
131
141
  documents: verified.documents,