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.
- package/README.md +121 -13
- package/assets/skill/SKILL.md +124 -31
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +31 -31
- package/assets/skills/metergraph-analyze/SKILL.md +161 -58
- package/assets/skills/metergraph-candidates/SKILL.md +50 -8
- package/assets/skills/metergraph-evals/SKILL.md +83 -16
- package/assets/skills/metergraph-investigate/SKILL.md +90 -34
- package/assets/skills/metergraph-iterate/SKILL.md +82 -12
- package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
- package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
- package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
- package/assets/skills/metergraph-report/SKILL.md +60 -15
- package/assets/skills/metergraph-workloads/SKILL.md +97 -13
- package/bin/metergraph.js +1 -1
- package/native/README.md +85 -0
- package/native/anchored-fs.c +243 -0
- package/native/bin/anchored-fs-darwin-arm64 +0 -0
- package/native/bin/anchored-fs-darwin-x64 +0 -0
- package/native/bin/anchored-fs-linux-arm64 +0 -0
- package/native/bin/anchored-fs-linux-x64 +0 -0
- package/native/build.mjs +55 -0
- package/native/manifest.json +9 -0
- package/native/targets.mjs +24 -0
- package/package.json +10 -3
- package/src/action-help.js +130 -0
- package/src/agent-key.js +22 -12
- package/src/anchored-fs.js +163 -0
- package/src/args.js +335 -31
- package/src/auth-binding.js +12 -7
- package/src/auth-callback.js +10 -2
- package/src/auth-login.js +111 -28
- package/src/auth-oauth.js +133 -36
- package/src/auth-session.js +19 -12
- package/src/auth-store.js +28 -7
- package/src/cli.js +147 -15
- package/src/constants.js +70 -1
- package/src/experiments-contract.js +462 -0
- package/src/experiments-output.js +190 -0
- package/src/experiments.js +406 -0
- package/src/output.js +374 -68
- package/src/pricing-args.js +163 -0
- package/src/pricing-output.js +206 -0
- package/src/pricing-reference.js +195 -0
- package/src/pricing.js +307 -0
- package/src/read-contract.js +64 -10
- package/src/read-output.js +66 -13
- package/src/read.js +105 -16
- package/src/repository-identity.js +2 -2
- package/src/setup-deployment.js +7 -0
- package/src/setup-env-git.js +1 -1
- package/src/setup-env.js +16 -9
- package/src/setup-interactive.js +356 -0
- package/src/setup-refusal.js +2 -2
- package/src/setup-state.js +9 -4
- package/src/setup.js +15 -3
- package/src/skill-bundle.js +2 -2
- package/src/skill-trusted.js +56 -0
- package/src/skill.js +28 -9
- package/src/style.js +33 -7
- package/src/tools-output.js +73 -0
- package/src/tools.js +249 -0
- package/src/trace-contract.js +1 -1
- package/src/transport.js +26 -3
- package/src/verify-output.js +2 -2
- 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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/native/build.mjs
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
24
|
-
"test
|
|
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
|
|
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
|
|
10
|
-
//
|
|
11
|
-
// environment as a secret. It
|
|
12
|
-
//
|
|
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
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
access: verified.access,
|
|
138
|
+
scopes: verified.scopes,
|
|
129
139
|
accessToken: token,
|
|
130
140
|
},
|
|
131
141
|
documents: verified.documents,
|